react_on_rails 16.0.0 → 16.0.1.rc.2

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +124 -77
  3. data/CLAUDE.md +46 -2
  4. data/CONTRIBUTING.md +12 -6
  5. data/Gemfile.development_dependencies +1 -0
  6. data/Gemfile.lock +3 -1
  7. data/LICENSE.md +15 -1
  8. data/README.md +68 -18
  9. data/bin/lefthook/check-trailing-newlines +38 -0
  10. data/bin/lefthook/get-changed-files +26 -0
  11. data/bin/lefthook/prettier-format +26 -0
  12. data/bin/lefthook/ruby-autofix +26 -0
  13. data/bin/lefthook/ruby-lint +27 -0
  14. data/eslint.config.ts +10 -0
  15. data/knip.ts +20 -9
  16. data/lib/generators/react_on_rails/USAGE +65 -0
  17. data/lib/generators/react_on_rails/base_generator.rb +7 -7
  18. data/lib/generators/react_on_rails/generator_helper.rb +4 -0
  19. data/lib/generators/react_on_rails/generator_messages.rb +2 -2
  20. data/lib/generators/react_on_rails/install_generator.rb +115 -7
  21. data/lib/generators/react_on_rails/react_no_redux_generator.rb +16 -4
  22. data/lib/generators/react_on_rails/react_with_redux_generator.rb +83 -14
  23. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.tsx +25 -0
  24. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.server.tsx +5 -0
  25. data/lib/generators/react_on_rails/templates/base/base/bin/dev +12 -24
  26. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/actions/helloWorldActionCreators.ts +18 -0
  27. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.tsx +24 -0
  28. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/constants/helloWorldConstants.ts +6 -0
  29. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/containers/HelloWorldContainer.ts +20 -0
  30. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/reducers/helloWorldReducer.ts +22 -0
  31. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.client.tsx +23 -0
  32. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.tsx +5 -0
  33. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/store/helloWorldStore.ts +18 -0
  34. data/lib/react_on_rails/configuration.rb +10 -6
  35. data/lib/react_on_rails/dev/server_manager.rb +185 -28
  36. data/lib/react_on_rails/doctor.rb +1149 -0
  37. data/lib/react_on_rails/helper.rb +9 -78
  38. data/lib/react_on_rails/pro/NOTICE +21 -0
  39. data/lib/react_on_rails/pro/helper.rb +122 -0
  40. data/lib/react_on_rails/pro/utils.rb +53 -0
  41. data/lib/react_on_rails/react_component/render_options.rb +6 -2
  42. data/lib/react_on_rails/system_checker.rb +659 -0
  43. data/lib/react_on_rails/version.rb +1 -1
  44. data/lib/tasks/doctor.rake +48 -0
  45. data/lib/tasks/generate_packs.rake +127 -4
  46. data/package-lock.json +11984 -0
  47. metadata +26 -6
  48. data/lib/generators/react_on_rails/bin/dev +0 -46
@@ -18,29 +18,17 @@
18
18
  # 3. Extend ReactOnRails::Dev classes in your Rails app for advanced customization
19
19
  # 4. Use classes directly: ReactOnRails::Dev::ServerManager.start(:development, "Custom.procfile")
20
20
 
21
- begin
22
- require "bundler/setup"
23
- require "react_on_rails/dev"
24
- rescue LoadError
25
- # Fallback for when gem is not yet installed
26
- puts "Loading ReactOnRails development tools..."
27
- require_relative "../../lib/react_on_rails/dev"
28
- end
21
+ require "bundler/setup"
22
+ require "react_on_rails/dev"
23
+
24
+ # Default route configuration
25
+ # This is set by the ReactOnRails installer to point to your generated component.
26
+ # Change this to your preferred default route, or pass --route=<route> to override.
27
+ DEFAULT_ROUTE = "hello_world"
29
28
 
30
29
  # Main execution
31
- case ARGV[0]
32
- when "production-assets", "prod"
33
- ReactOnRails::Dev::ServerManager.start(:production_like)
34
- when "static"
35
- ReactOnRails::Dev::ServerManager.start(:static, "Procfile.dev-static-assets")
36
- when "kill"
37
- ReactOnRails::Dev::ServerManager.kill_processes
38
- when "help", "--help", "-h"
39
- ReactOnRails::Dev::ServerManager.show_help
40
- when "hmr", nil
41
- ReactOnRails::Dev::ServerManager.start(:development, "Procfile.dev")
42
- else
43
- puts "Unknown argument: #{ARGV[0]}"
44
- puts "Run 'bin/dev help' for usage information"
45
- exit 1
46
- end
30
+ # Add the default route to ARGV if no --route option is provided
31
+ argv_with_defaults = ARGV.dup
32
+ argv_with_defaults.push("--route", DEFAULT_ROUTE) unless argv_with_defaults.any? { |arg| arg.start_with?("--route") }
33
+
34
+ ReactOnRails::Dev::ServerManager.run_from_command_line(argv_with_defaults)
@@ -0,0 +1,18 @@
1
+ /* eslint-disable import/prefer-default-export */
2
+
3
+ import { HELLO_WORLD_NAME_UPDATE } from '../constants/helloWorldConstants';
4
+
5
+ // Action interface
6
+ export interface UpdateNameAction {
7
+ type: typeof HELLO_WORLD_NAME_UPDATE;
8
+ text: string;
9
+ }
10
+
11
+ // Union type for all actions
12
+ export type HelloWorldAction = UpdateNameAction;
13
+
14
+ // Action creator with proper TypeScript typing
15
+ export const updateName = (text: string): UpdateNameAction => ({
16
+ type: HELLO_WORLD_NAME_UPDATE,
17
+ text,
18
+ });
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import * as style from './HelloWorld.module.css';
3
+ import type { PropsFromRedux } from '../containers/HelloWorldContainer';
4
+
5
+ // Component props are inferred from Redux container
6
+ type HelloWorldProps = PropsFromRedux;
7
+
8
+ const HelloWorld: React.FC<HelloWorldProps> = ({ name, updateName }) => (
9
+ <div>
10
+ <h3>
11
+ Hello,
12
+ {name}!
13
+ </h3>
14
+ <hr />
15
+ <form>
16
+ <label className={style.bright} htmlFor="name">
17
+ Say hello to:
18
+ <input id="name" type="text" value={name} onChange={(e) => updateName(e.target.value)} />
19
+ </label>
20
+ </form>
21
+ </div>
22
+ );
23
+
24
+ export default HelloWorld;
@@ -0,0 +1,6 @@
1
+ /* eslint-disable import/prefer-default-export */
2
+
3
+ export const HELLO_WORLD_NAME_UPDATE = 'HELLO_WORLD_NAME_UPDATE' as const;
4
+
5
+ // Action type for TypeScript
6
+ export type HelloWorldActionType = typeof HELLO_WORLD_NAME_UPDATE;
@@ -0,0 +1,20 @@
1
+ // Simple example of a React "smart" component
2
+
3
+ import { connect, ConnectedProps } from 'react-redux';
4
+ import HelloWorld from '../components/HelloWorld';
5
+ import * as actions from '../actions/helloWorldActionCreators';
6
+ import type { HelloWorldState } from '../reducers/helloWorldReducer';
7
+
8
+ // Which part of the Redux global state does our component want to receive as props?
9
+ const mapStateToProps = (state: HelloWorldState) => ({ name: state.name });
10
+
11
+ // Create the connector
12
+ const connector = connect(mapStateToProps, actions);
13
+
14
+ // Infer the props from Redux state and actions
15
+ export type PropsFromRedux = ConnectedProps<typeof connector>;
16
+
17
+ // Don't forget to actually use connect!
18
+ // Note that we don't export HelloWorld, but the redux "connected" version of it.
19
+ // See https://github.com/reactjs/react-redux/blob/master/docs/api.md#examples
20
+ export default connector(HelloWorld);
@@ -0,0 +1,22 @@
1
+ import { combineReducers } from 'redux';
2
+ import { HELLO_WORLD_NAME_UPDATE } from '../constants/helloWorldConstants';
3
+ import { HelloWorldAction } from '../actions/helloWorldActionCreators';
4
+
5
+ // State interface
6
+ export interface HelloWorldState {
7
+ name: string;
8
+ }
9
+
10
+ // Individual reducer with TypeScript types
11
+ const name = (state: string = '', action: HelloWorldAction): string => {
12
+ switch (action.type) {
13
+ case HELLO_WORLD_NAME_UPDATE:
14
+ return action.text;
15
+ default:
16
+ return state;
17
+ }
18
+ };
19
+
20
+ const helloWorldReducer = combineReducers<HelloWorldState>({ name });
21
+
22
+ export default helloWorldReducer;
@@ -0,0 +1,23 @@
1
+ import { useMemo, type FC } from 'react';
2
+ import { Provider } from 'react-redux';
3
+
4
+ import configureStore, { type RailsProps } from '../store/helloWorldStore';
5
+ import HelloWorldContainer from '../containers/HelloWorldContainer';
6
+
7
+ // Props interface matches what Rails will pass from the controller
8
+ interface HelloWorldAppProps extends RailsProps {}
9
+
10
+ // See documentation for https://github.com/reactjs/react-redux.
11
+ // This is how you get props from the Rails view into the redux store.
12
+ // This code here binds your smart component to the redux store.
13
+ const HelloWorldApp: FC<HelloWorldAppProps> = (props) => {
14
+ const store = useMemo(() => configureStore(props), [props]);
15
+
16
+ return (
17
+ <Provider store={store}>
18
+ <HelloWorldContainer />
19
+ </Provider>
20
+ );
21
+ };
22
+
23
+ export default HelloWorldApp;
@@ -0,0 +1,5 @@
1
+ import HelloWorldApp from './HelloWorldApp.client';
2
+ // This could be specialized for server rendering
3
+ // For example, if using React Router, we'd have the SSR setup here.
4
+
5
+ export default HelloWorldApp;
@@ -0,0 +1,18 @@
1
+ import { createStore } from 'redux';
2
+ import type { Store, PreloadedState } from 'redux';
3
+ import helloWorldReducer from '../reducers/helloWorldReducer';
4
+ import type { HelloWorldState } from '../reducers/helloWorldReducer';
5
+
6
+ // Rails props interface - customize based on your Rails controller
7
+ export interface RailsProps {
8
+ name: string;
9
+ [key: string]: any; // Allow additional props from Rails
10
+ }
11
+
12
+ // Store type
13
+ export type HelloWorldStore = Store<HelloWorldState>;
14
+
15
+ const configureStore = (railsProps: RailsProps): HelloWorldStore =>
16
+ createStore(helloWorldReducer, railsProps as PreloadedState<HelloWorldState>);
17
+
18
+ export default configureStore;
@@ -175,7 +175,7 @@ module ReactOnRails
175
175
  end
176
176
 
177
177
  msg = <<~MSG
178
- ReactOnRails: Your current version of #{ReactOnRails::PackerUtils.packer_type.upcase_first} \
178
+ ReactOnRails: Your current version of shakapacker \
179
179
  does not support async script loading, which may cause performance issues. Please either:
180
180
  1. Use :sync or :defer loading strategy instead of :async
181
181
  2. Upgrade to Shakapacker v8.2.0 or above to enable async script loading
@@ -284,8 +284,10 @@ module ReactOnRails
284
284
  if ReactOnRails::PackerUtils.using_packer?
285
285
  packer_public_output_path = ReactOnRails::PackerUtils.packer_public_output_path
286
286
  # rubocop:disable Layout/LineLength
287
+ packer_name = ReactOnRails::PackerUtils.packer_type&.upcase_first
288
+
287
289
  Rails.logger.warn "Error configuring config/initializers/react_on_rails. Define neither the generated_assets_dirs nor " \
288
- "the generated_assets_dir when using #{ReactOnRails::PackerUtils.packer_type.upcase_first}. This is defined by " \
290
+ "the generated_assets_dir when using #{packer_name}. This is defined by " \
289
291
  "public_output_path specified in #{ReactOnRails::PackerUtils.packer_type}.yml = #{packer_public_output_path}."
290
292
  # rubocop:enable Layout/LineLength
291
293
  return
@@ -331,15 +333,17 @@ module ReactOnRails
331
333
  end
332
334
 
333
335
  def compile_command_conflict_message
336
+ packer_name = ReactOnRails::PackerUtils.packer_type.upcase_first
337
+ packer_type = ReactOnRails::PackerUtils.packer_type
334
338
  <<~MSG
335
339
 
336
- React on Rails and #{ReactOnRails::PackerUtils.packer_type.upcase_first} error in configuration!
340
+ React on Rails and #{packer_name} error in configuration!
337
341
  In order to use config/react_on_rails.rb config.build_production_command,
338
- you must edit config/#{ReactOnRails::PackerUtils.packer_type}.yml to include this value in the default configuration:
339
- '#{ReactOnRails::PackerUtils.packer_type}_precompile: false'
342
+ you must edit config/#{packer_type}.yml to include this value in the default configuration:
343
+ '#{packer_type}_precompile: false'
340
344
 
341
345
  Alternatively, remove the config/react_on_rails.rb config.build_production_command and the
342
- default bin/#{ReactOnRails::PackerUtils.packer_type} script will be used for assets:precompile.
346
+ default bin/#{packer_type} script will be used for assets:precompile.
343
347
 
344
348
  MSG
345
349
  end
@@ -8,16 +8,16 @@ module ReactOnRails
8
8
  module Dev
9
9
  class ServerManager
10
10
  class << self
11
- def start(mode = :development, procfile = nil, verbose: false)
11
+ def start(mode = :development, procfile = nil, verbose: false, route: nil, rails_env: nil)
12
12
  case mode
13
13
  when :production_like
14
- run_production_like(_verbose: verbose)
14
+ run_production_like(_verbose: verbose, route: route, rails_env: rails_env)
15
15
  when :static
16
16
  procfile ||= "Procfile.dev-static-assets"
17
- run_static_development(procfile, verbose: verbose)
17
+ run_static_development(procfile, verbose: verbose, route: route)
18
18
  when :development, :hmr
19
19
  procfile ||= "Procfile.dev"
20
- run_development(procfile, verbose: verbose)
20
+ run_development(procfile, verbose: verbose, route: route)
21
21
  else
22
22
  raise ArgumentError, "Unknown mode: #{mode}"
23
23
  end
@@ -116,6 +116,50 @@ module ReactOnRails
116
116
  puts help_troubleshooting
117
117
  end
118
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
+
119
163
  private
120
164
 
121
165
  def help_usage
@@ -142,12 +186,21 @@ module ReactOnRails
142
186
  end
143
187
  # rubocop:enable Metrics/AbcSize
144
188
 
189
+ # rubocop:disable Metrics/AbcSize
145
190
  def help_options
146
191
  <<~OPTIONS
147
192
  #{Rainbow('⚙️ OPTIONS:').cyan.bold}
148
- #{Rainbow('--verbose, -v').green.bold} #{Rainbow('Enable verbose output for pack generation').white}
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}
149
201
  OPTIONS
150
202
  end
203
+ # rubocop:enable Metrics/AbcSize
151
204
 
152
205
  def help_customization
153
206
  <<~CUSTOMIZATION
@@ -172,7 +225,7 @@ module ReactOnRails
172
225
  #{Rainbow('•').yellow} #{Rainbow('Source maps for debugging').white}
173
226
  #{Rainbow('•').yellow} #{Rainbow('May have Flash of Unstyled Content (FOUC)').white}
174
227
  #{Rainbow('•').yellow} #{Rainbow('Fast recompilation').white}
175
- #{Rainbow('•').yellow} #{Rainbow('Access at:').white} #{Rainbow('http://localhost:3000/hello_world').cyan.underline}
228
+ #{Rainbow('•').yellow} #{Rainbow('Access at:').white} #{Rainbow('http://localhost:3000/<route>').cyan.underline}
176
229
 
177
230
  #{Rainbow('📦 Static development mode').cyan.bold} - #{Rainbow('Procfile.dev-static-assets').green}:
178
231
  #{Rainbow('•').yellow} #{Rainbow('No HMR (static assets with auto-recompilation)').white}
@@ -181,24 +234,26 @@ module ReactOnRails
181
234
  #{Rainbow('•').yellow} #{Rainbow('CSS extracted to separate files (no FOUC)').white}
182
235
  #{Rainbow('•').yellow} #{Rainbow('Development environment (faster builds than production)').white}
183
236
  #{Rainbow('•').yellow} #{Rainbow('Source maps for debugging').white}
184
- #{Rainbow('•').yellow} #{Rainbow('Access at:').white} #{Rainbow('http://localhost:3000/hello_world').cyan.underline}
237
+ #{Rainbow('•').yellow} #{Rainbow('Access at:').white} #{Rainbow('http://localhost:3000/<route>').cyan.underline}
185
238
 
186
239
  #{Rainbow('🏭 Production-assets mode').cyan.bold} - #{Rainbow('Procfile.dev-prod-assets').green}:
187
240
  #{Rainbow('•').yellow} #{Rainbow('React on Rails pack generation before Procfile start').white}
188
- #{Rainbow('•').yellow} #{Rainbow('Asset precompilation with production optimizations').white}
189
- #{Rainbow('•').yellow} #{Rainbow('Optimized, minified bundles').white}
190
- #{Rainbow('•').yellow} #{Rainbow('Extracted CSS files (no FOUC)').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}
191
246
  #{Rainbow('•').yellow} #{Rainbow('No HMR (static assets)').white}
192
- #{Rainbow('•').yellow} #{Rainbow('Slower recompilation').white}
193
- #{Rainbow('•').yellow} #{Rainbow('Access at:').white} #{Rainbow('http://localhost:3001/hello_world').cyan.underline}
247
+ #{Rainbow('•').yellow} #{Rainbow('Access at:').white} #{Rainbow('http://localhost:3001/<route>').cyan.underline}
194
248
  MODES
195
249
  end
196
250
  # rubocop:enable Metrics/AbcSize
197
251
 
198
- def run_production_like(_verbose: false)
252
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
253
+ def run_production_like(_verbose: false, route: nil, rails_env: nil)
199
254
  procfile = "Procfile.dev-prod-assets"
200
255
 
201
- print_procfile_info(procfile)
256
+ print_procfile_info(procfile, route: route)
202
257
  print_server_info(
203
258
  "🏭 Starting production-like development server...",
204
259
  [
@@ -208,25 +263,124 @@ module ReactOnRails
208
263
  "No HMR (Hot Module Replacement)",
209
264
  "CSS extracted to separate files (no FOUC)"
210
265
  ],
211
- 3001
266
+ 3001,
267
+ route: route
212
268
  )
213
269
 
214
- # Precompile assets in production mode (includes pack generation automatically)
215
- puts "🔨 Precompiling assets..."
216
- success = system "RAILS_ENV=production NODE_ENV=production bundle exec rails assets:precompile"
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 ""
217
304
 
218
- if success
305
+ # Capture both stdout and stderr
306
+ require "open3"
307
+ stdout, stderr, status = Open3.capture3(env, *argv)
308
+
309
+ if status.success?
219
310
  puts "✅ Assets precompiled successfully"
220
311
  ProcessManager.ensure_procfile(procfile)
221
312
  ProcessManager.run_with_process_manager(procfile)
222
313
  else
223
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 ""
224
377
  exit 1
225
378
  end
226
379
  end
380
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
227
381
 
228
- def run_static_development(procfile, verbose: false)
229
- print_procfile_info(procfile)
382
+ def run_static_development(procfile, verbose: false, route: nil)
383
+ print_procfile_info(procfile, route: route)
230
384
  print_server_info(
231
385
  "⚡ Starting development server with static assets...",
232
386
  [
@@ -235,7 +389,8 @@ module ReactOnRails
235
389
  "CSS extracted to separate files (no FOUC)",
236
390
  "Development environment (source maps, faster builds)",
237
391
  "Auto-recompiles on file changes"
238
- ]
392
+ ],
393
+ route: route
239
394
  )
240
395
 
241
396
  PackGenerator.generate(verbose: verbose)
@@ -243,25 +398,27 @@ module ReactOnRails
243
398
  ProcessManager.run_with_process_manager(procfile)
244
399
  end
245
400
 
246
- def run_development(procfile, verbose: false)
247
- print_procfile_info(procfile)
401
+ def run_development(procfile, verbose: false, route: nil)
402
+ print_procfile_info(procfile, route: route)
248
403
  PackGenerator.generate(verbose: verbose)
249
404
  ProcessManager.ensure_procfile(procfile)
250
405
  ProcessManager.run_with_process_manager(procfile)
251
406
  end
252
407
 
253
- def print_server_info(title, features, port = 3000)
408
+ def print_server_info(title, features, port = 3000, route: nil)
254
409
  puts title
255
410
  features.each { |feature| puts " - #{feature}" }
256
411
  puts ""
257
412
  puts ""
258
- puts "💡 Access at: #{Rainbow("http://localhost:#{port}/hello_world").cyan.underline}"
413
+ url = route ? "http://localhost:#{port}/#{route}" : "http://localhost:#{port}"
414
+ puts "💡 Access at: #{Rainbow(url).cyan.underline}"
259
415
  puts ""
260
416
  end
261
417
 
262
- def print_procfile_info(procfile)
418
+ def print_procfile_info(procfile, route: nil)
263
419
  port = procfile_port(procfile)
264
420
  box_width = 60
421
+ url = route ? "http://localhost:#{port}/#{route}" : "http://localhost:#{port}"
265
422
 
266
423
  puts ""
267
424
  puts box_border(box_width)
@@ -269,7 +426,7 @@ module ReactOnRails
269
426
  puts format_box_line("📋 Using Procfile: #{procfile}", box_width)
270
427
  puts format_box_line("🔧 Customize this file for your app's needs", box_width)
271
428
  puts box_empty_line(box_width)
272
- puts format_box_line("💡 Access at: #{Rainbow("http://localhost:#{port}/hello_world").cyan.underline}",
429
+ puts format_box_line("💡 Access at: #{Rainbow(url).cyan.underline}",
273
430
  box_width)
274
431
  puts box_empty_line(box_width)
275
432
  puts box_bottom(box_width)