react_on_rails 14.2.1 → 16.1.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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/AI_AGENT_INSTRUCTIONS.md +63 -0
  3. data/CHANGELOG.md +564 -85
  4. data/CLAUDE.md +135 -0
  5. data/CODING_AGENTS.md +313 -0
  6. data/CONTRIBUTING.md +448 -37
  7. data/Gemfile.development_dependencies +6 -1
  8. data/Gemfile.lock +13 -4
  9. data/KUDOS.md +22 -1
  10. data/LICENSE.md +30 -4
  11. data/LICENSES/README.md +14 -0
  12. data/NEWS.md +48 -48
  13. data/PROJECTS.md +45 -40
  14. data/REACT-ON-RAILS-PRO-LICENSE.md +129 -0
  15. data/README.md +113 -42
  16. data/SUMMARY.md +62 -52
  17. data/TODO.md +135 -0
  18. data/bin/lefthook/check-trailing-newlines +38 -0
  19. data/bin/lefthook/get-changed-files +26 -0
  20. data/bin/lefthook/prettier-format +26 -0
  21. data/bin/lefthook/ruby-autofix +26 -0
  22. data/bin/lefthook/ruby-lint +27 -0
  23. data/eslint.config.ts +232 -0
  24. data/knip.ts +40 -6
  25. data/lib/generators/USAGE +4 -5
  26. data/lib/generators/react_on_rails/USAGE +65 -0
  27. data/lib/generators/react_on_rails/base_generator.rb +276 -62
  28. data/lib/generators/react_on_rails/dev_tests_generator.rb +1 -0
  29. data/lib/generators/react_on_rails/generator_helper.rb +35 -1
  30. data/lib/generators/react_on_rails/generator_messages.rb +138 -17
  31. data/lib/generators/react_on_rails/install_generator.rb +474 -26
  32. data/lib/generators/react_on_rails/react_no_redux_generator.rb +19 -6
  33. data/lib/generators/react_on_rails/react_with_redux_generator.rb +110 -18
  34. data/lib/generators/react_on_rails/templates/.eslintrc +1 -1
  35. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev +5 -0
  36. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev-prod-assets +8 -0
  37. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev-static-assets +2 -0
  38. data/lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx +0 -5
  39. data/lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorld.module.css +2 -2
  40. data/lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorldServer.js +1 -1
  41. data/lib/generators/react_on_rails/templates/base/base/app/javascript/packs/server-bundle.js +1 -8
  42. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.jsx +21 -0
  43. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.tsx +25 -0
  44. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css +4 -0
  45. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.server.jsx +5 -0
  46. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.server.tsx +5 -0
  47. data/lib/generators/react_on_rails/templates/base/base/app/views/hello_world/index.html.erb.tt +1 -1
  48. data/lib/generators/react_on_rails/templates/base/base/app/views/layouts/hello_world.html.erb +4 -2
  49. data/lib/generators/react_on_rails/templates/base/base/babel.config.js.tt +5 -2
  50. data/lib/generators/react_on_rails/templates/base/base/bin/dev +34 -0
  51. data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +14 -5
  52. data/lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml +76 -7
  53. data/lib/generators/react_on_rails/templates/base/base/config/webpack/commonWebpackConfig.js.tt +1 -1
  54. data/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt +6 -10
  55. data/lib/generators/react_on_rails/templates/base/base/config/webpack/production.js.tt +2 -2
  56. data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +3 -2
  57. data/lib/generators/react_on_rails/templates/base/base/config/webpack/test.js.tt +2 -2
  58. data/lib/generators/react_on_rails/templates/dev_tests/spec/system/hello_world_spec.rb +0 -2
  59. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/actions/helloWorldActionCreators.ts +18 -0
  60. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx +0 -6
  61. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.module.css +4 -0
  62. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.tsx +24 -0
  63. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/constants/helloWorldConstants.ts +6 -0
  64. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/containers/HelloWorldContainer.ts +20 -0
  65. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/reducers/helloWorldReducer.js +1 -1
  66. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/reducers/helloWorldReducer.ts +22 -0
  67. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.client.tsx +23 -0
  68. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.jsx +5 -0
  69. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.tsx +5 -0
  70. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/store/helloWorldStore.ts +18 -0
  71. data/lib/react_on_rails/configuration.rb +141 -57
  72. data/lib/react_on_rails/controller.rb +6 -2
  73. data/lib/react_on_rails/dev/file_manager.rb +78 -0
  74. data/lib/react_on_rails/dev/pack_generator.rb +27 -0
  75. data/lib/react_on_rails/dev/process_manager.rb +61 -0
  76. data/lib/react_on_rails/dev/server_manager.rb +487 -0
  77. data/lib/react_on_rails/dev.rb +20 -0
  78. data/lib/react_on_rails/doctor.rb +1149 -0
  79. data/lib/react_on_rails/engine.rb +6 -0
  80. data/lib/react_on_rails/git_utils.rb +12 -2
  81. data/lib/react_on_rails/helper.rb +176 -74
  82. data/lib/react_on_rails/json_parse_error.rb +6 -1
  83. data/lib/react_on_rails/packer_utils.rb +61 -71
  84. data/lib/react_on_rails/packs_generator.rb +221 -19
  85. data/lib/react_on_rails/prerender_error.rb +4 -0
  86. data/lib/react_on_rails/pro/NOTICE +21 -0
  87. data/lib/react_on_rails/pro/helper.rb +122 -0
  88. data/lib/react_on_rails/pro/utils.rb +53 -0
  89. data/lib/react_on_rails/react_component/render_options.rb +38 -6
  90. data/lib/react_on_rails/server_rendering_js_code.rb +0 -1
  91. data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +12 -5
  92. data/lib/react_on_rails/system_checker.rb +659 -0
  93. data/lib/react_on_rails/test_helper/webpack_assets_compiler.rb +1 -1
  94. data/lib/react_on_rails/test_helper/webpack_assets_status_checker.rb +6 -4
  95. data/lib/react_on_rails/test_helper.rb +2 -3
  96. data/lib/react_on_rails/utils.rb +139 -43
  97. data/lib/react_on_rails/version.rb +1 -1
  98. data/lib/react_on_rails/version_checker.rb +14 -20
  99. data/lib/react_on_rails/version_syntax_converter.rb +1 -1
  100. data/lib/react_on_rails.rb +1 -0
  101. data/lib/tasks/assets.rake +1 -1
  102. data/lib/tasks/doctor.rake +48 -0
  103. data/lib/tasks/generate_packs.rake +158 -1
  104. data/react_on_rails.gemspec +1 -0
  105. data/tsconfig.eslint.json +6 -0
  106. data/tsconfig.json +5 -3
  107. metadata +63 -14
  108. data/REACT-ON-RAILS-PRO-LICENSE +0 -95
  109. data/lib/generators/react_on_rails/adapt_for_older_shakapacker_generator.rb +0 -41
  110. data/lib/generators/react_on_rails/bin/dev +0 -30
  111. data/lib/generators/react_on_rails/bin/dev-static +0 -30
  112. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev-static.tt +0 -9
  113. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev.tt +0 -5
  114. data/lib/generators/react_on_rails/templates/base/base/app/javascript/packs/registration.js.tt +0 -8
  115. /data/lib/generators/react_on_rails/templates/base/base/config/webpack/{webpackConfig.js.tt → generateWebpackConfigs.js.tt} +0 -0
  116. /data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/{HelloWorldApp.jsx → HelloWorldApp.client.jsx} +0 -0
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails/generators"
4
+ require "fileutils"
4
5
  require_relative "generator_messages"
5
6
  require_relative "generator_helper"
6
7
  module ReactOnRails
7
8
  module Generators
8
9
  class BaseGenerator < Rails::Generators::Base
9
10
  include GeneratorHelper
11
+
10
12
  Rails::Generators.hide_namespace(namespace)
11
13
  source_root(File.expand_path("templates", __dir__))
12
14
 
@@ -14,7 +16,7 @@ module ReactOnRails
14
16
  class_option :redux,
15
17
  type: :boolean,
16
18
  default: false,
17
- desc: "Install Redux gems and Redux version of Hello World Example",
19
+ desc: "Install Redux package and Redux version of Hello World Example",
18
20
  aliases: "-R"
19
21
 
20
22
  def add_hello_world_route
@@ -22,28 +24,35 @@ module ReactOnRails
22
24
  end
23
25
 
24
26
  def create_react_directories
25
- dirs = %w[components]
26
- dirs.each { |name| empty_directory("app/javascript/bundles/HelloWorld/#{name}") }
27
+ # Create auto-bundling directory structure for non-Redux components only
28
+ # Redux components handle their own directory structure
29
+ return if options.redux?
30
+
31
+ empty_directory("app/javascript/src/HelloWorld/ror_components")
27
32
  end
28
33
 
29
34
  def copy_base_files
30
35
  base_path = "base/base/"
31
36
  base_files = %w[app/controllers/hello_world_controller.rb
32
- app/views/layouts/hello_world.html.erb]
33
- base_templates = %w[config/initializers/react_on_rails.rb
34
- Procfile.dev
35
- Procfile.dev-static]
37
+ app/views/layouts/hello_world.html.erb
38
+ Procfile.dev
39
+ Procfile.dev-static-assets
40
+ Procfile.dev-prod-assets]
41
+ base_templates = %w[config/initializers/react_on_rails.rb]
36
42
  base_files.each { |file| copy_file("#{base_path}#{file}", file) }
37
43
  base_templates.each do |file|
38
- template("#{base_path}/#{file}.tt", file, { packer_type: ReactOnRails::PackerUtils.packer_type })
44
+ template("#{base_path}/#{file}.tt", file)
39
45
  end
40
46
  end
41
47
 
42
48
  def copy_js_bundle_files
43
49
  base_path = "base/base/"
44
- base_files = %w[app/javascript/packs/server-bundle.js
45
- app/javascript/bundles/HelloWorld/components/HelloWorldServer.js
46
- app/javascript/bundles/HelloWorld/components/HelloWorld.module.css]
50
+ base_files = %w[app/javascript/packs/server-bundle.js]
51
+
52
+ # Only copy HelloWorld.module.css for non-Redux components
53
+ # Redux components handle their own CSS files
54
+ base_files << "app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css" unless options.redux?
55
+
47
56
  base_files.each { |file| copy_file("#{base_path}#{file}", file) }
48
57
  end
49
58
 
@@ -57,15 +66,25 @@ module ReactOnRails
57
66
  config/webpack/development.js
58
67
  config/webpack/production.js
59
68
  config/webpack/serverWebpackConfig.js
60
- config/webpack/webpack.config.js
61
- config/webpack/webpackConfig.js]
69
+ config/webpack/generateWebpackConfigs.js]
62
70
  config = {
63
71
  message: "// The source code including full typescript support is available at:"
64
72
  }
65
73
  base_files.each { |file| template("#{base_path}/#{file}.tt", file, config) }
74
+
75
+ # Handle webpack.config.js separately with smart replacement
76
+ copy_webpack_main_config(base_path, config)
66
77
  end
67
78
 
68
79
  def copy_packer_config
80
+ # Skip copying if Shakapacker was just installed (to avoid conflicts)
81
+ # Check for a temporary marker file that indicates fresh Shakapacker install
82
+ if File.exist?(".shakapacker_just_installed")
83
+ puts "Skipping Shakapacker config copy (already installed by Shakapacker installer)"
84
+ File.delete(".shakapacker_just_installed") # Clean up marker
85
+ return
86
+ end
87
+
69
88
  puts "Adding Shakapacker #{ReactOnRails::PackerUtils.shakapacker_version} config"
70
89
  base_path = "base/base/"
71
90
  config = "config/shakapacker.yml"
@@ -76,40 +95,23 @@ module ReactOnRails
76
95
  run "bundle"
77
96
  end
78
97
 
79
- def add_js_dependencies
80
- major_minor_patch_only = /\A\d+\.\d+\.\d+\z/
81
- if ReactOnRails::VERSION.match?(major_minor_patch_only)
82
- package_json.manager.add(["react-on-rails@#{ReactOnRails::VERSION}"])
83
- else
84
- # otherwise add latest
85
- puts "Adding the latest react-on-rails NPM module. Double check this is correct in package.json"
86
- package_json.manager.add(["react-on-rails"])
87
- end
98
+ def update_gitignore_for_generated_bundles
99
+ gitignore_path = File.join(destination_root, ".gitignore")
100
+ return unless File.exist?(gitignore_path)
88
101
 
89
- puts "Adding React dependencies"
90
- package_json.manager.add([
91
- "react",
92
- "react-dom",
93
- "@babel/preset-react",
94
- "prop-types",
95
- "babel-plugin-transform-react-remove-prop-types",
96
- "babel-plugin-macros"
97
- ])
102
+ gitignore_content = File.read(gitignore_path)
98
103
 
99
- puts "Adding CSS handlers"
104
+ additions = []
105
+ additions << "**/generated/**" unless gitignore_content.include?("**/generated/**")
106
+ additions << "ssr-generated" unless gitignore_content.include?("ssr-generated")
100
107
 
101
- package_json.manager.add(%w[
102
- css-loader
103
- css-minimizer-webpack-plugin
104
- mini-css-extract-plugin
105
- style-loader
106
- ])
108
+ return if additions.empty?
107
109
 
108
- puts "Adding dev dependencies"
109
- package_json.manager.add([
110
- "@pmmmwh/react-refresh-webpack-plugin",
111
- "react-refresh"
112
- ], type: :dev)
110
+ append_to_file ".gitignore" do
111
+ lines = ["\n# Generated React on Rails packs"]
112
+ lines.concat(additions)
113
+ "#{lines.join("\n")}\n"
114
+ end
113
115
  end
114
116
 
115
117
  def append_to_spec_rails_helper
@@ -118,25 +120,7 @@ module ReactOnRails
118
120
  add_configure_rspec_to_compile_assets(rails_helper)
119
121
  else
120
122
  spec_helper = File.join(destination_root, "spec/spec_helper.rb")
121
- if File.exist?(spec_helper)
122
- add_configure_rspec_to_compile_assets(spec_helper)
123
- else
124
- # rubocop:disable Layout/EmptyLinesAroundArguments
125
- GeneratorMessages.add_info(
126
- <<-MSG.strip_heredoc
127
-
128
- We did not find a spec/rails_helper.rb or spec/spec_helper.rb to add
129
- the React on Rails Test helper, which ensures that if we are running
130
- js tests, then we are using latest webpack assets. You can later add
131
- this to your rspec config:
132
-
133
- # This will use the defaults of :js and :server_rendering meta tags
134
- ReactOnRails::TestHelper.configure_rspec_to_compile_assets(config)
135
- MSG
136
- )
137
- # rubocop:enable Layout/EmptyLinesAroundArguments
138
-
139
- end
123
+ add_configure_rspec_to_compile_assets(spec_helper) if File.exist?(spec_helper)
140
124
  end
141
125
  end
142
126
 
@@ -145,10 +129,240 @@ module ReactOnRails
145
129
  # Ensure that if we are running js tests, we are using latest webpack assets
146
130
  # This will use the defaults of :js and :server_rendering meta tags
147
131
  ReactOnRails::TestHelper.configure_rspec_to_compile_assets(config)
132
+ end
148
133
  STR
149
134
 
150
135
  private
151
136
 
137
+ def setup_js_dependencies
138
+ add_js_dependencies
139
+ install_js_dependencies
140
+ end
141
+
142
+ def add_js_dependencies
143
+ add_react_on_rails_package
144
+ add_react_dependencies
145
+ add_css_dependencies
146
+ add_dev_dependencies
147
+ end
148
+
149
+ def add_react_on_rails_package
150
+ major_minor_patch_only = /\A\d+\.\d+\.\d+\z/
151
+
152
+ # Try to use package_json gem first, fall back to direct npm commands
153
+ react_on_rails_pkg = if ReactOnRails::VERSION.match?(major_minor_patch_only)
154
+ ["react-on-rails@#{ReactOnRails::VERSION}"]
155
+ else
156
+ puts "Adding the latest react-on-rails NPM module. " \
157
+ "Double check this is correct in package.json"
158
+ ["react-on-rails"]
159
+ end
160
+
161
+ puts "Installing React on Rails package..."
162
+ return if add_npm_dependencies(react_on_rails_pkg)
163
+
164
+ puts "Using direct npm commands as fallback"
165
+ success = system("npm", "install", *react_on_rails_pkg)
166
+ handle_npm_failure("react-on-rails package", react_on_rails_pkg) unless success
167
+ end
168
+
169
+ def add_react_dependencies
170
+ puts "Installing React dependencies..."
171
+ react_deps = %w[
172
+ react
173
+ react-dom
174
+ @babel/preset-react
175
+ prop-types
176
+ babel-plugin-transform-react-remove-prop-types
177
+ babel-plugin-macros
178
+ ]
179
+ return if add_npm_dependencies(react_deps)
180
+
181
+ success = system("npm", "install", *react_deps)
182
+ handle_npm_failure("React dependencies", react_deps) unless success
183
+ end
184
+
185
+ def add_css_dependencies
186
+ puts "Installing CSS handling dependencies..."
187
+ css_deps = %w[
188
+ css-loader
189
+ css-minimizer-webpack-plugin
190
+ mini-css-extract-plugin
191
+ style-loader
192
+ ]
193
+ return if add_npm_dependencies(css_deps)
194
+
195
+ success = system("npm", "install", *css_deps)
196
+ handle_npm_failure("CSS dependencies", css_deps) unless success
197
+ end
198
+
199
+ def add_dev_dependencies
200
+ puts "Installing development dependencies..."
201
+ dev_deps = %w[
202
+ @pmmmwh/react-refresh-webpack-plugin
203
+ react-refresh
204
+ ]
205
+ return if add_npm_dependencies(dev_deps, dev: true)
206
+
207
+ success = system("npm", "install", "--save-dev", *dev_deps)
208
+ handle_npm_failure("development dependencies", dev_deps, dev: true) unless success
209
+ end
210
+
211
+ def install_js_dependencies
212
+ # Detect which package manager to use
213
+ success = if File.exist?(File.join(destination_root, "yarn.lock"))
214
+ system("yarn", "install")
215
+ elsif File.exist?(File.join(destination_root, "pnpm-lock.yaml"))
216
+ system("pnpm", "install")
217
+ elsif File.exist?(File.join(destination_root, "package-lock.json")) ||
218
+ File.exist?(File.join(destination_root, "package.json"))
219
+ # Use npm for package-lock.json or as default fallback
220
+ system("npm", "install")
221
+ else
222
+ true # No package manager detected, skip
223
+ end
224
+
225
+ unless success
226
+ GeneratorMessages.add_warning(<<~MSG.strip)
227
+ ⚠️ JavaScript dependencies installation failed.
228
+
229
+ This could be due to network issues or missing package manager.
230
+ You can install dependencies manually later by running:
231
+ • npm install (if using npm)
232
+ • yarn install (if using yarn)
233
+ • pnpm install (if using pnpm)
234
+ MSG
235
+ end
236
+
237
+ success
238
+ end
239
+
240
+ def handle_npm_failure(dependency_type, packages, dev: false)
241
+ install_command = dev ? "npm install --save-dev" : "npm install"
242
+ GeneratorMessages.add_warning(<<~MSG.strip)
243
+ ⚠️ Failed to install #{dependency_type}.
244
+
245
+ The following packages could not be installed automatically:
246
+ #{packages.map { |pkg| " • #{pkg}" }.join("\n")}
247
+
248
+ This could be due to network issues or missing package manager.
249
+ You can install them manually later by running:
250
+ #{install_command} #{packages.join(' ')}
251
+ MSG
252
+ end
253
+
254
+ def copy_webpack_main_config(base_path, config)
255
+ webpack_config_path = "config/webpack/webpack.config.js"
256
+
257
+ if File.exist?(webpack_config_path)
258
+ existing_content = File.read(webpack_config_path)
259
+
260
+ # Check if it's the standard Shakapacker config that we can safely replace
261
+ if standard_shakapacker_config?(existing_content)
262
+ # Remove the file first to avoid conflict prompt, then recreate it
263
+ remove_file(webpack_config_path, verbose: false)
264
+ # Show what we're doing
265
+ puts " #{set_color('replace', :green)} #{webpack_config_path} " \
266
+ "(auto-upgrading from standard Shakapacker to React on Rails config)"
267
+ template("#{base_path}/#{webpack_config_path}.tt", webpack_config_path, config)
268
+ elsif react_on_rails_config?(existing_content)
269
+ puts " #{set_color('identical', :blue)} #{webpack_config_path} " \
270
+ "(already React on Rails compatible)"
271
+ # Skip - don't need to do anything
272
+ else
273
+ handle_custom_webpack_config(base_path, config, webpack_config_path)
274
+ end
275
+ else
276
+ # File doesn't exist, create it
277
+ template("#{base_path}/#{webpack_config_path}.tt", webpack_config_path, config)
278
+ end
279
+ end
280
+
281
+ def handle_custom_webpack_config(base_path, config, webpack_config_path)
282
+ # Custom config - ask user
283
+ puts "\n#{set_color('NOTICE:', :yellow)} Your webpack.config.js appears to be customized."
284
+ puts "React on Rails needs to replace it with an environment-specific loader."
285
+ puts "Your current config will be backed up to webpack.config.js.backup"
286
+
287
+ if yes?("Replace webpack.config.js with React on Rails version? (Y/n)")
288
+ # Create backup
289
+ backup_path = "#{webpack_config_path}.backup"
290
+ if File.exist?(webpack_config_path)
291
+ FileUtils.cp(webpack_config_path, backup_path)
292
+ puts " #{set_color('create', :green)} #{backup_path} (backup of your custom config)"
293
+ end
294
+
295
+ template("#{base_path}/#{webpack_config_path}.tt", webpack_config_path, config)
296
+ else
297
+ puts " #{set_color('skip', :yellow)} #{webpack_config_path}"
298
+ puts " #{set_color('WARNING:', :red)} React on Rails may not work correctly " \
299
+ "without the environment-specific webpack config"
300
+ end
301
+ end
302
+
303
+ def standard_shakapacker_config?(content)
304
+ # Get the expected default config based on Shakapacker version
305
+ expected_configs = shakapacker_default_configs
306
+
307
+ # Check if the content matches any of the known default configurations
308
+ expected_configs.any? { |config| content_matches_template?(content, config) }
309
+ end
310
+
311
+ def content_matches_template?(content, template)
312
+ # Normalize whitespace and compare
313
+ normalize_config_content(content) == normalize_config_content(template)
314
+ end
315
+
316
+ def normalize_config_content(content)
317
+ # Remove comments, normalize whitespace, and clean up for comparison
318
+ content.gsub(%r{//.*$}, "") # Remove single-line comments
319
+ .gsub(%r{/\*.*?\*/}m, "") # Remove multi-line comments
320
+ .gsub(/\s+/, " ") # Normalize whitespace
321
+ .strip
322
+ end
323
+
324
+ def shakapacker_default_configs
325
+ configs = []
326
+
327
+ # Shakapacker v7+ (generateWebpackConfig function)
328
+ configs << <<~CONFIG
329
+ // See the shakacode/shakapacker README and docs directory for advice on customizing your webpackConfig.
330
+ const { generateWebpackConfig } = require('shakapacker')
331
+
332
+ const webpackConfig = generateWebpackConfig()
333
+
334
+ module.exports = webpackConfig
335
+ CONFIG
336
+
337
+ # Shakapacker v6 (webpackConfig object)
338
+ configs << <<~CONFIG
339
+ const { webpackConfig } = require('shakapacker')
340
+
341
+ // See the shakacode/shakapacker README and docs directory for advice on customizing your webpackConfig.
342
+
343
+ module.exports = webpackConfig
344
+ CONFIG
345
+
346
+ # Also check without comments for variations
347
+ configs << <<~CONFIG
348
+ const { generateWebpackConfig } = require('shakapacker')
349
+ const webpackConfig = generateWebpackConfig()
350
+ module.exports = webpackConfig
351
+ CONFIG
352
+
353
+ configs << <<~CONFIG
354
+ const { webpackConfig } = require('shakapacker')
355
+ module.exports = webpackConfig
356
+ CONFIG
357
+
358
+ configs
359
+ end
360
+
361
+ def react_on_rails_config?(content)
362
+ # Check if it already has React on Rails environment-specific loading
363
+ content.include?("envSpecificConfig") || content.include?("env.nodeEnv")
364
+ end
365
+
152
366
  # From https://github.com/rails/rails/blob/4c940b2dbfb457f67c6250b720f63501d74a45fd/railties/lib/rails/generators/rails/app/app_generator.rb
153
367
  def app_name
154
368
  @app_name ||= (defined_app_const_base? ? defined_app_name : File.basename(destination_root))
@@ -7,6 +7,7 @@ module ReactOnRails
7
7
  module Generators
8
8
  class DevTestsGenerator < Rails::Generators::Base
9
9
  include GeneratorHelper
10
+
10
11
  Rails::Generators.hide_namespace(namespace)
11
12
  source_root(File.expand_path("templates/dev_tests", __dir__))
12
13
 
@@ -1,11 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "package_json"
4
3
  require "rainbow"
4
+ require "json"
5
5
 
6
6
  module GeneratorHelper
7
7
  def package_json
8
+ # Lazy load package_json gem only when actually needed for dependency management
9
+
10
+ require "package_json" unless defined?(PackageJson)
8
11
  @package_json ||= PackageJson.read
12
+ rescue LoadError
13
+ puts "Warning: package_json gem not available. This is expected before Shakapacker installation."
14
+ puts "Dependencies will be installed using the default package manager after Shakapacker setup."
15
+ nil
16
+ rescue StandardError => e
17
+ puts "Warning: Could not read package.json: #{e.message}"
18
+ puts "This is normal before Shakapacker creates the package.json file."
19
+ nil
20
+ end
21
+
22
+ # Safe wrapper for package_json operations
23
+ def add_npm_dependencies(packages, dev: false)
24
+ pj = package_json
25
+ return false unless pj
26
+
27
+ begin
28
+ if dev
29
+ pj.manager.add(packages, type: :dev)
30
+ else
31
+ pj.manager.add(packages)
32
+ end
33
+ true
34
+ rescue StandardError => e
35
+ puts "Warning: Could not add packages via package_json gem: #{e.message}"
36
+ puts "Will fall back to direct npm commands."
37
+ false
38
+ end
9
39
  end
10
40
 
11
41
  # Takes a relative path from the destination root, such as `.gitignore` or `app/assets/javascripts/application.js`
@@ -61,4 +91,8 @@ module GeneratorHelper
61
91
  def add_documentation_reference(message, source)
62
92
  "#{message} \n#{source}"
63
93
  end
94
+
95
+ def component_extension(options)
96
+ options.typescript? ? "tsx" : "jsx"
97
+ end
64
98
  end
@@ -38,37 +38,158 @@ module GeneratorMessages
38
38
  @output = []
39
39
  end
40
40
 
41
- def helpful_message_after_installation
41
+ def helpful_message_after_installation(component_name: "HelloWorld", route: "hello_world")
42
+ process_manager_section = build_process_manager_section
43
+ testing_section = build_testing_section
44
+ package_manager = detect_package_manager
45
+ shakapacker_status = build_shakapacker_status_section
46
+
42
47
  <<~MSG
43
48
 
44
- What to do next:
49
+ ╔════════════════════════════════════════════════════════════════════════╗
50
+ ║ 🎉 React on Rails Successfully Installed! ║
51
+ ╚════════════════════════════════════════════════════════════════════════╝
52
+ #{process_manager_section}#{shakapacker_status}
53
+
54
+ 📋 QUICK START:
55
+ ─────────────────────────────────────────────────────────────────────────
56
+ 1. Install dependencies:
57
+ #{Rainbow("bundle && #{package_manager} install").cyan}
58
+
59
+ 2. Start the app:
60
+ ./bin/dev # HMR (Hot Module Replacement) mode
61
+ ./bin/dev static # Static bundles (no HMR, faster initial load)
62
+ ./bin/dev prod # Production-like mode for testing
63
+ ./bin/dev help # See all available options
64
+
65
+ 3. Visit: #{Rainbow(route ? "http://localhost:3000/#{route}" : 'http://localhost:3000').cyan.underline}
66
+ ✨ KEY FEATURES:
67
+ ─────────────────────────────────────────────────────────────────────────
68
+ • Auto-registration enabled - Your layout only needs:
69
+ <%= javascript_pack_tag %>
70
+ <%= stylesheet_pack_tag %>
71
+
72
+ • Server-side rendering - Enabled with prerender option in app/views/hello_world/index.html.erb:
73
+ <%= react_component("#{component_name}", props: @hello_world_props, prerender: true) %>
74
+
75
+ 📚 LEARN MORE:
76
+ ─────────────────────────────────────────────────────────────────────────
77
+ • Documentation: #{Rainbow('https://www.shakacode.com/react-on-rails/docs/').cyan.underline}
78
+ • Webpack customization: #{Rainbow('https://github.com/shakacode/shakapacker#webpack-configuration').cyan.underline}
79
+
80
+ 💡 TIP: Run 'bin/dev help' for development server options and troubleshooting#{testing_section}
81
+ MSG
82
+ end
45
83
 
46
- - See the documentation on https://github.com/shakacode/shakapacker#webpack-configuration
47
- for how to customize the default webpack configuration.
84
+ private
85
+
86
+ def build_process_manager_section
87
+ process_manager = detect_process_manager
88
+ if process_manager
89
+ if process_manager == "overmind"
90
+ "\n📦 #{Rainbow("#{process_manager} detected ✓").green} " \
91
+ "#{Rainbow('(Recommended for easier debugging)').blue}"
92
+ else
93
+ "\n📦 #{Rainbow("#{process_manager} detected ✓").green}"
94
+ end
95
+ else
96
+ <<~INSTALL
97
+
98
+ ⚠️ No process manager detected. Install one:
99
+ #{Rainbow('brew install overmind').yellow.bold} # Recommended (easier debugging)
100
+ #{Rainbow('gem install foreman').yellow} # Alternative
101
+ INSTALL
102
+ end
103
+ end
48
104
 
49
- - Include your webpack assets to your application layout.
105
+ def build_testing_section
106
+ # Check if we have any spec files to determine if testing setup is needed
107
+ has_spec_files = File.exist?("spec/rails_helper.rb") || File.exist?("spec/spec_helper.rb")
50
108
 
51
- <%= javascript_pack_tag 'hello-world-bundle' %>
109
+ return "" if has_spec_files
52
110
 
53
- - To start Rails server run:
111
+ <<~TESTING
54
112
 
55
- ./bin/dev # Running with HMR
56
113
 
57
- or
114
+ 🧪 TESTING SETUP (Optional):
115
+ ─────────────────────────────────────────────────────────────────────────
116
+ For JavaScript testing with asset compilation, add this to your RSpec config:
58
117
 
59
- ./bin/dev-static # Running with statically created bundles, without HMR
118
+ # In spec/rails_helper.rb or spec/spec_helper.rb:
119
+ ReactOnRails::TestHelper.configure_rspec_to_compile_assets(config)
120
+ TESTING
121
+ end
60
122
 
61
- - To server render, change this line app/views/hello_world/index.html.erb to
62
- `prerender: true` to see server rendering (right click on page and select "view source").
123
+ def detect_process_manager
124
+ if system("which overmind > /dev/null 2>&1")
125
+ "overmind"
126
+ elsif system("which foreman > /dev/null 2>&1")
127
+ "foreman"
128
+ end
129
+ end
63
130
 
64
- <%= react_component("HelloWorldApp", props: @hello_world_props, prerender: true) %>
131
+ def build_shakapacker_status_section
132
+ version_warning = check_shakapacker_version_warning
133
+
134
+ if File.exist?(".shakapacker_just_installed")
135
+ base_message = <<~SHAKAPACKER
136
+
137
+ 📦 SHAKAPACKER SETUP:
138
+ ─────────────────────────────────────────────────────────────────────────
139
+ #{Rainbow('✓ Added to Gemfile automatically').green}
140
+ #{Rainbow('✓ Installer ran successfully').green}
141
+ #{Rainbow('✓ Webpack integration configured').green}
142
+ SHAKAPACKER
143
+ base_message + version_warning
144
+ elsif File.exist?("bin/shakapacker") && File.exist?("bin/shakapacker-dev-server")
145
+ "\n📦 #{Rainbow('Shakapacker already configured ✓').green}#{version_warning}"
146
+ else
147
+ "\n📦 #{Rainbow('Shakapacker setup may be incomplete').yellow}#{version_warning}"
148
+ end
149
+ end
65
150
 
66
- Alternative steps to run the app:
151
+ def check_shakapacker_version_warning
152
+ # Try to detect Shakapacker version from Gemfile.lock
153
+ return "" unless File.exist?("Gemfile.lock")
67
154
 
68
- - We recommend using Procfile.dev with foreman, overmind, or a similar program. Alternately, you can run each of the processes listed in Procfile.dev in a separate tab in your terminal.
155
+ gemfile_lock_content = File.read("Gemfile.lock")
156
+ shakapacker_match = gemfile_lock_content.match(/shakapacker \((\d+\.\d+\.\d+)\)/)
69
157
 
70
- - Visit http://localhost:3000/hello_world and see your React On Rails app running!
71
- MSG
158
+ return "" unless shakapacker_match
159
+
160
+ version = shakapacker_match[1]
161
+ major_version = version.split(".").first.to_i
162
+
163
+ if major_version < 8
164
+ <<~WARNING
165
+
166
+ ⚠️ #{Rainbow('IMPORTANT: Upgrade Recommended').yellow.bold}
167
+ ─────────────────────────────────────────────────────────────────────────
168
+ You are using Shakapacker #{version}. React on Rails v15+ works best with
169
+ Shakapacker 8.0+ for optimal Hot Module Replacement and build performance.
170
+
171
+ To upgrade: #{Rainbow('bundle update shakapacker').cyan}
172
+
173
+ Learn more: #{Rainbow('https://github.com/shakacode/shakapacker').cyan.underline}
174
+ WARNING
175
+ else
176
+ ""
177
+ end
178
+ rescue StandardError
179
+ # If version detection fails, don't show a warning to avoid noise
180
+ ""
181
+ end
182
+
183
+ def detect_package_manager
184
+ # Check for lock files to determine package manager
185
+ if File.exist?("yarn.lock")
186
+ "yarn"
187
+ elsif File.exist?("pnpm-lock.yaml")
188
+ "pnpm"
189
+ else
190
+ # Default to npm (Shakapacker 8.x default) - covers package-lock.json and no lockfile
191
+ "npm"
192
+ end
72
193
  end
73
194
  end
74
195
  end