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
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReactOnRails
4
+ module Dev
5
+ class ProcessManager
6
+ class << self
7
+ def installed?(process)
8
+ IO.popen([process, "-v"], &:close)
9
+ true
10
+ rescue Errno::ENOENT
11
+ false
12
+ end
13
+
14
+ def ensure_procfile(procfile)
15
+ return if File.exist?(procfile)
16
+
17
+ warn <<~MSG
18
+ ERROR:
19
+ Please ensure `#{procfile}` exists in your project!
20
+ MSG
21
+ exit 1
22
+ end
23
+
24
+ def run_with_process_manager(procfile)
25
+ # Validate procfile path for security
26
+ unless valid_procfile_path?(procfile)
27
+ warn "ERROR: Invalid procfile path: #{procfile}"
28
+ exit 1
29
+ end
30
+
31
+ # Clean up stale files before starting
32
+ FileManager.cleanup_stale_files
33
+
34
+ if installed?("overmind")
35
+ system("overmind", "start", "-f", procfile)
36
+ elsif installed?("foreman")
37
+ system("foreman", "start", "-f", procfile)
38
+ else
39
+ warn <<~MSG
40
+ NOTICE:
41
+ For this script to run, you need either 'overmind' or 'foreman' installed on your machine. Please try this script after installing one of them.
42
+ MSG
43
+ exit 1
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def valid_procfile_path?(procfile)
50
+ # Reject paths with shell metacharacters
51
+ return false if procfile.match?(/[;&|`$(){}\[\]<>]/)
52
+
53
+ # Ensure it's a readable file
54
+ File.readable?(procfile)
55
+ rescue StandardError
56
+ false
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,487 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "English"
4
+ require "open3"
5
+ require "rainbow"
6
+
7
+ module ReactOnRails
8
+ module Dev
9
+ class ServerManager
10
+ class << self
11
+ def start(mode = :development, procfile = nil, verbose: false, route: nil, rails_env: nil)
12
+ case mode
13
+ when :production_like
14
+ run_production_like(_verbose: verbose, route: route, rails_env: rails_env)
15
+ when :static
16
+ procfile ||= "Procfile.dev-static-assets"
17
+ run_static_development(procfile, verbose: verbose, route: route)
18
+ when :development, :hmr
19
+ procfile ||= "Procfile.dev"
20
+ run_development(procfile, verbose: verbose, route: route)
21
+ else
22
+ raise ArgumentError, "Unknown mode: #{mode}"
23
+ end
24
+ end
25
+
26
+ def kill_processes
27
+ puts "🔪 Killing all development processes..."
28
+ puts ""
29
+
30
+ killed_any = kill_running_processes || cleanup_socket_files
31
+
32
+ print_kill_summary(killed_any)
33
+ end
34
+
35
+ def development_processes
36
+ {
37
+ "rails" => "Rails server",
38
+ "node.*react[-_]on[-_]rails" => "React on Rails Node processes",
39
+ "overmind" => "Overmind process manager",
40
+ "foreman" => "Foreman process manager",
41
+ "ruby.*puma" => "Puma server",
42
+ "webpack-dev-server" => "Webpack dev server",
43
+ "bin/shakapacker-dev-server" => "Shakapacker dev server"
44
+ }
45
+ end
46
+
47
+ def kill_running_processes
48
+ killed_any = false
49
+
50
+ development_processes.each do |pattern, description|
51
+ pids = find_process_pids(pattern)
52
+ next unless pids.any?
53
+
54
+ puts " ☠️ Killing #{description} (PIDs: #{pids.join(', ')})"
55
+ terminate_processes(pids)
56
+ killed_any = true
57
+ end
58
+
59
+ killed_any
60
+ end
61
+
62
+ def find_process_pids(pattern)
63
+ stdout, _status = Open3.capture2("pgrep", "-f", pattern, err: File::NULL)
64
+ stdout.split("\n").map(&:to_i).reject { |pid| pid == Process.pid }
65
+ rescue Errno::ENOENT
66
+ # pgrep command not found
67
+ []
68
+ end
69
+
70
+ def terminate_processes(pids)
71
+ pids.each do |pid|
72
+ Process.kill("TERM", pid)
73
+ rescue StandardError
74
+ nil
75
+ end
76
+ end
77
+
78
+ def cleanup_socket_files
79
+ files = [".overmind.sock", "tmp/sockets/overmind.sock", "tmp/pids/server.pid"]
80
+ killed_any = false
81
+
82
+ files.each do |file|
83
+ next unless File.exist?(file)
84
+
85
+ puts " 🧹 Removing #{file}"
86
+ File.delete(file)
87
+ killed_any = true
88
+ rescue StandardError
89
+ nil
90
+ end
91
+
92
+ killed_any
93
+ end
94
+
95
+ def print_kill_summary(killed_any)
96
+ if killed_any
97
+ puts ""
98
+ puts "✅ All processes terminated and sockets cleaned"
99
+ puts "💡 You can now run 'bin/dev' for a clean start"
100
+ else
101
+ puts " ℹ️ No development processes found running"
102
+ end
103
+ end
104
+
105
+ def show_help
106
+ puts help_usage
107
+ puts ""
108
+ puts help_commands
109
+ puts ""
110
+ puts help_options
111
+ puts ""
112
+ puts help_customization
113
+ puts ""
114
+ puts help_mode_details
115
+ puts ""
116
+ puts help_troubleshooting
117
+ end
118
+
119
+ def run_from_command_line(args = ARGV)
120
+ require "optparse"
121
+
122
+ options = { route: nil, rails_env: nil }
123
+
124
+ OptionParser.new do |opts|
125
+ opts.banner = "Usage: dev [command] [options]"
126
+
127
+ opts.on("--route ROUTE", "Specify the route to display in URLs (default: root)") do |route|
128
+ options[:route] = route
129
+ end
130
+
131
+ opts.on("--rails-env ENV", "Override RAILS_ENV for assets:precompile step only (prod mode only)") do |env|
132
+ options[:rails_env] = env
133
+ end
134
+
135
+ opts.on("-h", "--help", "Prints this help") do
136
+ show_help
137
+ exit
138
+ end
139
+ end.parse!(args)
140
+
141
+ # Get the command (anything that's not parsed as an option)
142
+ command = args[0]
143
+
144
+ # Main execution
145
+ case command
146
+ when "production-assets", "prod"
147
+ start(:production_like, nil, verbose: false, route: options[:route], rails_env: options[:rails_env])
148
+ when "static"
149
+ start(:static, "Procfile.dev-static-assets", verbose: false, route: options[:route])
150
+ when "kill"
151
+ kill_processes
152
+ when "help", "--help", "-h"
153
+ show_help
154
+ when "hmr", nil
155
+ start(:development, "Procfile.dev", verbose: false, route: options[:route])
156
+ else
157
+ puts "Unknown argument: #{command}"
158
+ puts "Run 'dev help' for usage information"
159
+ exit 1
160
+ end
161
+ end
162
+
163
+ private
164
+
165
+ def help_usage
166
+ Rainbow("📋 Usage: bin/dev [command] [options]").bold
167
+ end
168
+
169
+ # rubocop:disable Metrics/AbcSize
170
+ def help_commands
171
+ <<~COMMANDS
172
+ #{Rainbow('🚀 COMMANDS:').cyan.bold}
173
+ #{Rainbow('(none) / hmr').green.bold} #{Rainbow('Start development server with HMR (default)').white}
174
+ #{Rainbow('→ Uses:').yellow} Procfile.dev
175
+
176
+ #{Rainbow('static').green.bold} #{Rainbow('Start development server with static assets (no HMR, no FOUC)').white}
177
+ #{Rainbow('→ Uses:').yellow} Procfile.dev-static-assets
178
+
179
+ #{Rainbow('production-assets').green.bold} #{Rainbow('Start with production-optimized assets (no HMR)').white}
180
+ #{Rainbow('prod').green.bold} #{Rainbow('Alias for production-assets').white}
181
+ #{Rainbow('→ Uses:').yellow} Procfile.dev-prod-assets
182
+
183
+ #{Rainbow('kill').red.bold} #{Rainbow('Kill all development processes for a clean start').white}
184
+ #{Rainbow('help').blue.bold} #{Rainbow('Show this help message').white}
185
+ COMMANDS
186
+ end
187
+ # rubocop:enable Metrics/AbcSize
188
+
189
+ # rubocop:disable Metrics/AbcSize
190
+ def help_options
191
+ <<~OPTIONS
192
+ #{Rainbow('⚙️ OPTIONS:').cyan.bold}
193
+ #{Rainbow('--route ROUTE').green.bold} #{Rainbow('Specify route to display in URLs (default: root)').white}
194
+ #{Rainbow('--rails-env ENV').green.bold} #{Rainbow('Override RAILS_ENV for assets:precompile step only (prod mode only)').white}
195
+ #{Rainbow('--verbose, -v').green.bold} #{Rainbow('Enable verbose output for pack generation').white}
196
+
197
+ #{Rainbow('📝 EXAMPLES:').cyan.bold}
198
+ #{Rainbow('bin/dev prod').green.bold} #{Rainbow('# NODE_ENV=production, RAILS_ENV=development').white}
199
+ #{Rainbow('bin/dev prod --rails-env=production').green.bold} #{Rainbow('# NODE_ENV=production, RAILS_ENV=production').white}
200
+ #{Rainbow('bin/dev prod --route=dashboard').green.bold} #{Rainbow('# Custom route in URLs').white}
201
+ OPTIONS
202
+ end
203
+ # rubocop:enable Metrics/AbcSize
204
+
205
+ def help_customization
206
+ <<~CUSTOMIZATION
207
+ #{Rainbow('🔧 CUSTOMIZATION:').cyan.bold}
208
+ Each mode uses a specific Procfile that you can customize for your application:
209
+
210
+ #{Rainbow('•').yellow} #{Rainbow('Procfile.dev').green.bold} - HMR development with webpack-dev-server
211
+ #{Rainbow('•').yellow} #{Rainbow('Procfile.dev-static-assets').green.bold} - Static development with webpack --watch
212
+ #{Rainbow('•').yellow} #{Rainbow('Procfile.dev-prod-assets').green.bold} - Production-optimized assets (port 3001)
213
+
214
+ #{Rainbow('Edit these files to customize the development environment for your needs.').white}
215
+ CUSTOMIZATION
216
+ end
217
+
218
+ # rubocop:disable Metrics/AbcSize
219
+ def help_mode_details
220
+ <<~MODES
221
+ #{Rainbow('🔥 HMR Development mode (default)').cyan.bold} - #{Rainbow('Procfile.dev').green}:
222
+ #{Rainbow('•').yellow} #{Rainbow('Hot Module Replacement (HMR) enabled').white}
223
+ #{Rainbow('•').yellow} #{Rainbow('React on Rails pack generation before Procfile start').white}
224
+ #{Rainbow('•').yellow} #{Rainbow('Webpack dev server for fast recompilation').white}
225
+ #{Rainbow('•').yellow} #{Rainbow('Source maps for debugging').white}
226
+ #{Rainbow('•').yellow} #{Rainbow('May have Flash of Unstyled Content (FOUC)').white}
227
+ #{Rainbow('•').yellow} #{Rainbow('Fast recompilation').white}
228
+ #{Rainbow('•').yellow} #{Rainbow('Access at:').white} #{Rainbow('http://localhost:3000/<route>').cyan.underline}
229
+
230
+ #{Rainbow('📦 Static development mode').cyan.bold} - #{Rainbow('Procfile.dev-static-assets').green}:
231
+ #{Rainbow('•').yellow} #{Rainbow('No HMR (static assets with auto-recompilation)').white}
232
+ #{Rainbow('•').yellow} #{Rainbow('React on Rails pack generation before Procfile start').white}
233
+ #{Rainbow('•').yellow} #{Rainbow('Webpack watch mode for auto-recompilation').white}
234
+ #{Rainbow('•').yellow} #{Rainbow('CSS extracted to separate files (no FOUC)').white}
235
+ #{Rainbow('•').yellow} #{Rainbow('Development environment (faster builds than production)').white}
236
+ #{Rainbow('•').yellow} #{Rainbow('Source maps for debugging').white}
237
+ #{Rainbow('•').yellow} #{Rainbow('Access at:').white} #{Rainbow('http://localhost:3000/<route>').cyan.underline}
238
+
239
+ #{Rainbow('🏭 Production-assets mode').cyan.bold} - #{Rainbow('Procfile.dev-prod-assets').green}:
240
+ #{Rainbow('•').yellow} #{Rainbow('React on Rails pack generation before Procfile start').white}
241
+ #{Rainbow('•').yellow} #{Rainbow('Asset precompilation with NODE_ENV=production (webpack optimizations)').white}
242
+ #{Rainbow('•').yellow} #{Rainbow('RAILS_ENV=development by default for assets:precompile (avoids credentials)').white}
243
+ #{Rainbow('•').yellow} #{Rainbow('Use --rails-env=production for assets:precompile only (not server processes)').white}
244
+ #{Rainbow('•').yellow} #{Rainbow('Server processes controlled by Procfile.dev-prod-assets environment').white}
245
+ #{Rainbow('•').yellow} #{Rainbow('Optimized, minified bundles with CSS extraction').white}
246
+ #{Rainbow('•').yellow} #{Rainbow('No HMR (static assets)').white}
247
+ #{Rainbow('•').yellow} #{Rainbow('Access at:').white} #{Rainbow('http://localhost:3001/<route>').cyan.underline}
248
+ MODES
249
+ end
250
+ # rubocop:enable Metrics/AbcSize
251
+
252
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
253
+ def run_production_like(_verbose: false, route: nil, rails_env: nil)
254
+ procfile = "Procfile.dev-prod-assets"
255
+
256
+ print_procfile_info(procfile, route: route)
257
+ print_server_info(
258
+ "🏭 Starting production-like development server...",
259
+ [
260
+ "Generating React on Rails packs",
261
+ "Precompiling assets with production optimizations",
262
+ "Running Rails server on port 3001",
263
+ "No HMR (Hot Module Replacement)",
264
+ "CSS extracted to separate files (no FOUC)"
265
+ ],
266
+ 3001,
267
+ route: route
268
+ )
269
+
270
+ # Precompile assets with production webpack optimizations (includes pack generation automatically)
271
+ env = { "NODE_ENV" => "production" }
272
+
273
+ # Validate and sanitize rails_env to prevent shell injection
274
+ if rails_env
275
+ unless rails_env.match?(/\A[a-z0-9_]+\z/i)
276
+ puts "❌ Invalid rails_env: '#{rails_env}'. Must contain only letters, numbers, and underscores."
277
+ exit 1
278
+ end
279
+ env["RAILS_ENV"] = rails_env
280
+ end
281
+
282
+ argv = ["bundle", "exec", "rails", "assets:precompile"]
283
+
284
+ puts "🔨 Precompiling assets with production webpack optimizations..."
285
+ puts ""
286
+
287
+ puts Rainbow("ℹ️ Asset Precompilation Environment:").blue
288
+ puts " • NODE_ENV=production → Webpack optimizations (minification, compression)"
289
+ if rails_env
290
+ puts " • RAILS_ENV=#{rails_env} → Custom Rails environment for assets:precompile only"
291
+ puts " • Note: RAILS_ENV=production requires credentials, database setup, etc."
292
+ puts " • Server processes will use environment from Procfile.dev-prod-assets"
293
+ else
294
+ puts " • RAILS_ENV=development → Simpler Rails setup (no credentials needed)"
295
+ puts " • Use --rails-env=production for assets:precompile step only"
296
+ puts " • Server processes will use environment from Procfile.dev-prod-assets"
297
+ puts " • Gets production webpack bundles without production Rails complexity"
298
+ end
299
+ puts ""
300
+
301
+ env_display = env.map { |k, v| "#{k}=#{v}" }.join(" ")
302
+ puts "#{Rainbow('💻 Running:').blue} #{env_display} #{argv.join(' ')}"
303
+ puts ""
304
+
305
+ # Capture both stdout and stderr
306
+ require "open3"
307
+ stdout, stderr, status = Open3.capture3(env, *argv)
308
+
309
+ if status.success?
310
+ puts "✅ Assets precompiled successfully"
311
+ ProcessManager.ensure_procfile(procfile)
312
+ ProcessManager.run_with_process_manager(procfile)
313
+ else
314
+ puts "❌ Asset precompilation failed"
315
+ puts ""
316
+
317
+ # Combine and display all output
318
+ all_output = []
319
+ all_output << stdout unless stdout.empty?
320
+ all_output << stderr unless stderr.empty?
321
+
322
+ unless all_output.empty?
323
+ puts Rainbow("📋 Full Command Output:").red.bold
324
+ puts Rainbow("─" * 60).red
325
+ all_output.each { |output| puts output }
326
+ puts Rainbow("─" * 60).red
327
+ puts ""
328
+ end
329
+
330
+ puts Rainbow("🛠️ To debug this issue:").yellow.bold
331
+ command_display = "#{env_display} #{argv.join(' ')}"
332
+ puts "#{Rainbow('1.').cyan} #{Rainbow('Run the command separately to see detailed output:').white}"
333
+ puts " #{Rainbow(command_display).cyan}"
334
+ puts ""
335
+ puts "#{Rainbow('2.').cyan} #{Rainbow('Add --trace for full stack trace:').white}"
336
+ puts " #{Rainbow("#{command_display} --trace").cyan}"
337
+ puts ""
338
+ puts "#{Rainbow('3.').cyan} #{Rainbow('Or try with development webpack (faster, less optimized):').white}"
339
+ puts " #{Rainbow('NODE_ENV=development bundle exec rails assets:precompile').cyan}"
340
+ puts ""
341
+
342
+ puts Rainbow("💡 Common fixes:").yellow.bold
343
+
344
+ # Provide specific guidance based on error content
345
+ error_content = "#{stderr} #{stdout}".downcase
346
+
347
+ if error_content.include?("secret_key_base")
348
+ puts "#{Rainbow('•').yellow} #{Rainbow('Missing secret_key_base:').white.bold} " \
349
+ "Run #{Rainbow('bin/rails credentials:edit').cyan}"
350
+ end
351
+
352
+ if error_content.include?("database") || error_content.include?("relation") ||
353
+ error_content.include?("table")
354
+ puts "#{Rainbow('•').yellow} #{Rainbow('Database issues:').white.bold} " \
355
+ "Run #{Rainbow('bin/rails db:create db:migrate').cyan}"
356
+ end
357
+
358
+ if error_content.include?("gem") || error_content.include?("bundle") || error_content.include?("load error")
359
+ puts "#{Rainbow('•').yellow} #{Rainbow('Missing dependencies:').white.bold} " \
360
+ "Run #{Rainbow('bundle install && npm install').cyan}"
361
+ end
362
+
363
+ if error_content.include?("webpack") || error_content.include?("module") ||
364
+ error_content.include?("compilation")
365
+ puts "#{Rainbow('•').yellow} #{Rainbow('Webpack compilation:').white.bold} " \
366
+ "Check JavaScript/webpack errors above"
367
+ end
368
+
369
+ # Always show these general options
370
+ puts "#{Rainbow('•').yellow} #{Rainbow('Environment config:').white} " \
371
+ "Check #{Rainbow('config/environments/production.rb').cyan}"
372
+
373
+ puts ""
374
+ puts Rainbow("ℹ️ Alternative for development:").blue
375
+ puts " #{Rainbow('bin/dev static').green} # Static assets without production optimizations"
376
+ puts ""
377
+ exit 1
378
+ end
379
+ end
380
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
381
+
382
+ def run_static_development(procfile, verbose: false, route: nil)
383
+ print_procfile_info(procfile, route: route)
384
+ print_server_info(
385
+ "⚡ Starting development server with static assets...",
386
+ [
387
+ "Generating React on Rails packs",
388
+ "Using shakapacker --watch (no HMR)",
389
+ "CSS extracted to separate files (no FOUC)",
390
+ "Development environment (source maps, faster builds)",
391
+ "Auto-recompiles on file changes"
392
+ ],
393
+ route: route
394
+ )
395
+
396
+ PackGenerator.generate(verbose: verbose)
397
+ ProcessManager.ensure_procfile(procfile)
398
+ ProcessManager.run_with_process_manager(procfile)
399
+ end
400
+
401
+ def run_development(procfile, verbose: false, route: nil)
402
+ print_procfile_info(procfile, route: route)
403
+ PackGenerator.generate(verbose: verbose)
404
+ ProcessManager.ensure_procfile(procfile)
405
+ ProcessManager.run_with_process_manager(procfile)
406
+ end
407
+
408
+ def print_server_info(title, features, port = 3000, route: nil)
409
+ puts title
410
+ features.each { |feature| puts " - #{feature}" }
411
+ puts ""
412
+ puts ""
413
+ url = route ? "http://localhost:#{port}/#{route}" : "http://localhost:#{port}"
414
+ puts "💡 Access at: #{Rainbow(url).cyan.underline}"
415
+ puts ""
416
+ end
417
+
418
+ def print_procfile_info(procfile, route: nil)
419
+ port = procfile_port(procfile)
420
+ box_width = 60
421
+ url = route ? "http://localhost:#{port}/#{route}" : "http://localhost:#{port}"
422
+
423
+ puts ""
424
+ puts box_border(box_width)
425
+ puts box_empty_line(box_width)
426
+ puts format_box_line("📋 Using Procfile: #{procfile}", box_width)
427
+ puts format_box_line("🔧 Customize this file for your app's needs", box_width)
428
+ puts box_empty_line(box_width)
429
+ puts format_box_line("💡 Access at: #{Rainbow(url).cyan.underline}",
430
+ box_width)
431
+ puts box_empty_line(box_width)
432
+ puts box_bottom(box_width)
433
+ puts ""
434
+ end
435
+
436
+ def procfile_port(procfile)
437
+ procfile == "Procfile.dev-prod-assets" ? 3001 : 3000
438
+ end
439
+
440
+ def box_border(width)
441
+ "┌#{'─' * (width - 2)}┐"
442
+ end
443
+
444
+ def box_bottom(width)
445
+ "└#{'─' * (width - 2)}┘"
446
+ end
447
+
448
+ def box_empty_line(width)
449
+ "│#{' ' * (width - 2)}│"
450
+ end
451
+
452
+ def format_box_line(content, box_width)
453
+ line = "│ #{content}"
454
+ # Use visual length for colored text
455
+ visual_length = Rainbow.uncolor(line).length
456
+ padding = box_width - visual_length - 2
457
+ line + "#{' ' * padding}│"
458
+ end
459
+
460
+ # rubocop:disable Metrics/AbcSize
461
+ def help_troubleshooting
462
+ <<~TROUBLESHOOTING
463
+ #{Rainbow('🔧 TROUBLESHOOTING:').cyan.bold}
464
+
465
+ #{Rainbow('⚛️ React Refresh Issues:').yellow.bold}
466
+ #{Rainbow('If you see "$RefreshSig$ is not defined" errors:').white}
467
+ #{Rainbow('1.').green} #{Rainbow('Check that both babel plugin and webpack plugin are configured:').white}
468
+ #{Rainbow('•').yellow} #{Rainbow('babel.config.js: \'react-refresh/babel\' plugin (enabled when WEBPACK_SERVE=true)').white}
469
+ #{Rainbow('•').yellow} #{Rainbow('config/webpack/development.js: ReactRefreshWebpackPlugin (enabled when WEBPACK_SERVE=true)').white}
470
+ #{Rainbow('2.').green} #{Rainbow('Ensure you\'re running HMR mode:').white} #{Rainbow('bin/dev').green.bold} #{Rainbow('(not').white} #{Rainbow('bin/dev static').red}#{Rainbow(')').white}
471
+ #{Rainbow('3.').green} #{Rainbow('Try restarting the development server:').white} #{Rainbow('bin/dev kill && bin/dev').green.bold}
472
+ #{Rainbow('4.').green} #{Rainbow('Note: React Refresh only works in HMR mode, not static mode').white}
473
+
474
+ #{Rainbow('🚨 General Issues:').yellow.bold}
475
+ #{Rainbow('•').red} #{Rainbow('"Port already in use"').white} #{Rainbow('→ Run:').yellow} #{Rainbow('bin/dev kill').green.bold}
476
+ #{Rainbow('•').red} #{Rainbow('"Webpack compilation failed"').white} #{Rainbow('→ Check console for specific errors').white}
477
+ #{Rainbow('•').red} #{Rainbow('"Process manager not found"').white} #{Rainbow('→ Install:').yellow} #{Rainbow('brew install overmind').green.bold} #{Rainbow('(or').white} #{Rainbow('gem install foreman').green.bold}#{Rainbow(')').white}
478
+ #{Rainbow('•').red} #{Rainbow('"Assets not loading"').white} #{Rainbow('→ Verify Procfile.dev is present and check server logs').white}
479
+
480
+ #{Rainbow('📚 Need help? Visit:').blue.bold} #{Rainbow('https://www.shakacode.com/react-on-rails/docs/').cyan.underline}
481
+ TROUBLESHOOTING
482
+ end
483
+ # rubocop:enable Metrics/AbcSize
484
+ end
485
+ end
486
+ end
487
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dev/server_manager"
4
+ require_relative "dev/process_manager"
5
+ require_relative "dev/pack_generator"
6
+ require_relative "dev/file_manager"
7
+
8
+ module ReactOnRails
9
+ module Dev
10
+ # Development server management for React on Rails
11
+ #
12
+ # This module provides classes to manage development servers,
13
+ # process managers, pack generation, and file cleanup.
14
+ #
15
+ # Usage:
16
+ # ReactOnRails::Dev::ServerManager.start(:development)
17
+ # ReactOnRails::Dev::ServerManager.kill_processes
18
+ # ReactOnRails::Dev::ServerManager.show_help
19
+ end
20
+ end