react_on_rails 16.1.2 → 16.2.0

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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +2 -0
  3. data/.rubocop.yml +85 -0
  4. data/Gemfile.development_dependencies +8 -7
  5. data/Gemfile.lock +158 -119
  6. data/Steepfile +56 -0
  7. data/lib/generators/react_on_rails/base_generator.rb +43 -120
  8. data/lib/generators/react_on_rails/dev_tests_generator.rb +2 -1
  9. data/lib/generators/react_on_rails/generator_helper.rb +102 -2
  10. data/lib/generators/react_on_rails/install_generator.rb +36 -156
  11. data/lib/generators/react_on_rails/js_dependency_manager.rb +383 -0
  12. data/lib/generators/react_on_rails/templates/base/base/.dev-services.yml.example +76 -0
  13. data/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook +30 -0
  14. data/lib/generators/react_on_rails/templates/base/base/bin/switch-bundler +141 -0
  15. data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +44 -45
  16. data/lib/generators/react_on_rails/templates/base/base/config/{shakapacker.yml → shakapacker.yml.tt} +28 -3
  17. data/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt +15 -9
  18. data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +42 -6
  19. data/lib/react_on_rails/configuration.rb +149 -32
  20. data/lib/react_on_rails/controller.rb +3 -3
  21. data/lib/react_on_rails/dev/pack_generator.rb +168 -2
  22. data/lib/react_on_rails/dev/process_manager.rb +136 -14
  23. data/lib/react_on_rails/dev/server_manager.rb +194 -26
  24. data/lib/react_on_rails/dev/service_checker.rb +200 -0
  25. data/lib/react_on_rails/doctor.rb +341 -12
  26. data/lib/react_on_rails/engine.rb +75 -1
  27. data/lib/react_on_rails/git_utils.rb +3 -1
  28. data/lib/react_on_rails/helper.rb +70 -192
  29. data/lib/react_on_rails/locales/base.rb +17 -5
  30. data/lib/react_on_rails/packer_utils.rb +79 -2
  31. data/lib/react_on_rails/packs_generator.rb +57 -39
  32. data/lib/react_on_rails/prerender_error.rb +74 -17
  33. data/lib/react_on_rails/pro_helper.rb +64 -0
  34. data/lib/react_on_rails/react_component/render_options.rb +7 -7
  35. data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +2 -5
  36. data/lib/react_on_rails/smart_error.rb +326 -0
  37. data/lib/react_on_rails/system_checker.rb +8 -9
  38. data/lib/react_on_rails/test_helper/webpack_assets_status_checker.rb +16 -7
  39. data/lib/react_on_rails/utils.rb +241 -55
  40. data/lib/react_on_rails/version.rb +1 -1
  41. data/lib/react_on_rails/version_checker.rb +383 -35
  42. data/lib/tasks/generate_packs.rake +12 -6
  43. data/lib/tasks/locale.rake +6 -1
  44. data/rakelib/docker.rake +26 -0
  45. data/rakelib/dummy_apps.rake +30 -0
  46. data/rakelib/example_type.rb +121 -0
  47. data/rakelib/examples_config.yml +52 -0
  48. data/rakelib/lint.rake +52 -0
  49. data/rakelib/node_package.rake +15 -0
  50. data/rakelib/rbs.rake +70 -0
  51. data/rakelib/run_rspec.rake +223 -0
  52. data/rakelib/shakapacker_examples.rake +171 -0
  53. data/rakelib/task_helpers.rb +134 -0
  54. data/rakelib/update_changelog.rake +73 -0
  55. data/react_on_rails.gemspec +4 -3
  56. data/sig/README.md +52 -0
  57. data/sig/react_on_rails/configuration.rbs +96 -0
  58. data/sig/react_on_rails/controller.rbs +15 -0
  59. data/sig/react_on_rails/dev/file_manager.rbs +15 -0
  60. data/sig/react_on_rails/dev/pack_generator.rbs +19 -0
  61. data/sig/react_on_rails/dev/process_manager.rbs +22 -0
  62. data/sig/react_on_rails/dev/server_manager.rbs +39 -0
  63. data/sig/react_on_rails/dev/service_checker.rbs +22 -0
  64. data/sig/react_on_rails/error.rbs +4 -0
  65. data/sig/react_on_rails/generators/js_dependency_manager.rbs +123 -0
  66. data/sig/react_on_rails/git_utils.rbs +8 -0
  67. data/sig/react_on_rails/helper.rbs +65 -0
  68. data/sig/react_on_rails/json_parse_error.rbs +10 -0
  69. data/sig/react_on_rails/locales.rbs +46 -0
  70. data/sig/react_on_rails/packer_utils.rbs +15 -0
  71. data/sig/react_on_rails/prerender_error.rbs +21 -0
  72. data/sig/react_on_rails/server_rendering_pool.rbs +12 -0
  73. data/sig/react_on_rails/smart_error.rbs +28 -0
  74. data/sig/react_on_rails/test_helper.rbs +11 -0
  75. data/sig/react_on_rails/utils.rbs +34 -0
  76. data/sig/react_on_rails/version_checker.rbs +12 -0
  77. data/sig/react_on_rails.rbs +17 -0
  78. metadata +49 -32
  79. data/AI_AGENT_INSTRUCTIONS.md +0 -63
  80. data/CHANGELOG.md +0 -1836
  81. data/CLAUDE.md +0 -135
  82. data/CODING_AGENTS.md +0 -313
  83. data/CONTRIBUTING.md +0 -668
  84. data/Dockerfile_tests +0 -12
  85. data/KUDOS.md +0 -114
  86. data/LICENSE.md +0 -47
  87. data/LICENSES/README.md +0 -14
  88. data/NEWS.md +0 -62
  89. data/PROJECTS.md +0 -63
  90. data/REACT-ON-RAILS-PRO-LICENSE.md +0 -129
  91. data/README.md +0 -217
  92. data/SUMMARY.md +0 -88
  93. data/TODO.md +0 -135
  94. data/bin/lefthook/check-trailing-newlines +0 -38
  95. data/bin/lefthook/get-changed-files +0 -26
  96. data/bin/lefthook/prettier-format +0 -26
  97. data/bin/lefthook/ruby-autofix +0 -26
  98. data/bin/lefthook/ruby-lint +0 -27
  99. data/docker-compose.yml +0 -11
  100. data/eslint.config.ts +0 -232
  101. data/knip.ts +0 -114
  102. data/lib/react_on_rails/pro/NOTICE +0 -21
  103. data/lib/react_on_rails/pro/helper.rb +0 -122
  104. data/lib/react_on_rails/pro/utils.rb +0 -53
  105. data/tsconfig.eslint.json +0 -6
  106. data/tsconfig.json +0 -19
@@ -1,26 +1,192 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "English"
4
+ require "stringio"
5
+ require_relative "../packer_utils"
4
6
 
5
7
  module ReactOnRails
6
8
  module Dev
9
+ # PackGenerator triggers the generation of React on Rails packs
10
+ #
11
+ # Design decisions:
12
+ # 1. Why trigger via Rake task instead of direct Ruby code?
13
+ # - The actual pack generation logic lives in lib/react_on_rails/packs_generator.rb
14
+ # - The rake task (lib/tasks/generate_packs.rake) provides a stable, documented interface
15
+ # - This allows the implementation to evolve without breaking bin/dev
16
+ # - Users can also run the task directly: `rake react_on_rails:generate_packs`
17
+ #
18
+ # 2. Why two execution strategies (direct vs bundle exec)?
19
+ # - Direct Rake execution: Faster when already in Bundler/Rails context (bin/dev)
20
+ # - Bundle exec fallback: Required when called outside Rails context
21
+ # - This optimization avoids subprocess overhead in the common case
22
+ #
23
+ # 3. Why is the class named "PackGenerator" when it delegates?
24
+ # - It's a semantic wrapper around pack generation for the dev workflow
25
+ # - Provides a clean API for bin/dev without exposing Rake internals
26
+ # - Handles hook detection, error handling, and output formatting
7
27
  class PackGenerator
8
28
  class << self
9
29
  def generate(verbose: false)
30
+ # Skip if shakapacker has a precompile hook configured
31
+ if ReactOnRails::PackerUtils.shakapacker_precompile_hook_configured?
32
+ if verbose
33
+ hook_value = ReactOnRails::PackerUtils.shakapacker_precompile_hook_value
34
+ puts "⏭️ Skipping pack generation (handled by shakapacker precompile hook: #{hook_value})"
35
+ end
36
+ return
37
+ end
38
+
10
39
  if verbose
11
40
  puts "📦 Generating React on Rails packs..."
12
- success = system "bundle exec rake react_on_rails:generate_packs"
41
+ success = run_pack_generation(silent: false, verbose: true)
13
42
  else
14
43
  print "📦 Generating packs... "
15
- success = system "bundle exec rake react_on_rails:generate_packs > /dev/null 2>&1"
44
+ success = run_pack_generation(silent: true, verbose: false)
16
45
  puts success ? "✅" : "❌"
17
46
  end
18
47
 
19
48
  return if success
20
49
 
21
50
  puts "❌ Pack generation failed"
51
+ unless verbose
52
+ puts ""
53
+ puts "💡 Run with #{Rainbow('--verbose').cyan.bold} flag for detailed output:"
54
+ puts " #{Rainbow('bin/dev --verbose').green.bold}"
55
+ end
22
56
  exit 1
23
57
  end
58
+
59
+ private
60
+
61
+ def run_pack_generation(silent: false, verbose: false)
62
+ # Set environment variable for child processes to respect verbose mode
63
+ ENV["REACT_ON_RAILS_VERBOSE"] = verbose ? "true" : "false"
64
+
65
+ # If we're already inside a Bundler context AND Rails is available (e.g., called from bin/dev),
66
+ # we can directly require and run the task. Otherwise, use bundle exec.
67
+ if should_run_directly?
68
+ run_rake_task_directly(silent: silent)
69
+ else
70
+ run_via_bundle_exec(silent: silent, verbose: verbose)
71
+ end
72
+ ensure
73
+ # Clean up environment variable
74
+ ENV.delete("REACT_ON_RAILS_VERBOSE")
75
+ end
76
+
77
+ def should_run_directly?
78
+ # Check if we're in a meaningful Bundler context with BUNDLE_GEMFILE
79
+ return false unless defined?(Bundler)
80
+ return false unless ENV["BUNDLE_GEMFILE"]
81
+ return false unless rails_available?
82
+
83
+ true
84
+ end
85
+
86
+ def rails_available?
87
+ return false unless defined?(Rails)
88
+ return false unless Rails.respond_to?(:application)
89
+ return false if Rails.application.nil?
90
+
91
+ # Verify Rails app can actually load tasks
92
+ begin
93
+ Rails.application.respond_to?(:load_tasks)
94
+ rescue StandardError
95
+ false
96
+ end
97
+ end
98
+
99
+ def run_rake_task_directly(silent: false)
100
+ require "rake"
101
+
102
+ load_rake_tasks
103
+ task = prepare_rake_task
104
+
105
+ capture_output(silent) do
106
+ task.invoke
107
+ true
108
+ end
109
+ rescue StandardError => e
110
+ handle_rake_error(e, silent)
111
+ false
112
+ end
113
+
114
+ def load_rake_tasks
115
+ return if Rake::Task.task_defined?("react_on_rails:generate_packs")
116
+
117
+ Rails.application.load_tasks
118
+ end
119
+
120
+ def prepare_rake_task
121
+ task = Rake::Task["react_on_rails:generate_packs"]
122
+ task.reenable # Allow re-execution if called multiple times
123
+ task
124
+ end
125
+
126
+ def capture_output(silent)
127
+ return yield unless silent
128
+
129
+ original_stdout = $stdout
130
+ original_stderr = $stderr
131
+ output_buffer = StringIO.new
132
+ $stdout = output_buffer
133
+ $stderr = output_buffer
134
+
135
+ begin
136
+ yield
137
+ ensure
138
+ $stdout = original_stdout
139
+ $stderr = original_stderr
140
+ end
141
+ end
142
+
143
+ def handle_rake_error(error, _silent)
144
+ error_msg = "Error generating packs: #{error.message}"
145
+ error_msg += "\n#{error.backtrace.join("\n")}" if ENV["DEBUG"]
146
+
147
+ # Always write to stderr, even in silent mode
148
+ # Use STDERR constant instead of warn/$stderr to bypass capture_output redirection
149
+ # rubocop:disable Style/StderrPuts, Style/GlobalStdStream
150
+ STDERR.puts error_msg
151
+ # rubocop:enable Style/StderrPuts, Style/GlobalStdStream
152
+ end
153
+
154
+ def run_via_bundle_exec(silent: false, verbose: false)
155
+ # Environment variable is already set in run_pack_generation, but we make it explicit here
156
+ # for clarity and to ensure it's passed to the subprocess
157
+ env = { "REACT_ON_RAILS_VERBOSE" => verbose ? "true" : "false" }
158
+
159
+ # Need to unbundle to prevent Bundler from intercepting our bundle exec call
160
+ # when already running inside a Bundler context (e.g., from bin/dev)
161
+ with_unbundled_context do
162
+ if silent
163
+ system(
164
+ env,
165
+ "bundle", "exec", "rake", "react_on_rails:generate_packs",
166
+ out: File::NULL, err: File::NULL
167
+ )
168
+ else
169
+ system(env, "bundle", "exec", "rake", "react_on_rails:generate_packs")
170
+ end
171
+ end
172
+ end
173
+
174
+ # DRY helper method for Bundler context switching with API compatibility
175
+ # Supports both new (with_unbundled_env) and legacy (with_clean_env) Bundler APIs
176
+ def with_unbundled_context(&block)
177
+ if defined?(Bundler)
178
+ if Bundler.respond_to?(:with_unbundled_env)
179
+ Bundler.with_unbundled_env(&block)
180
+ elsif Bundler.respond_to?(:with_clean_env)
181
+ Bundler.with_clean_env(&block)
182
+ else
183
+ # Fallback if neither method is available (very old Bundler versions)
184
+ yield
185
+ end
186
+ else
187
+ yield
188
+ end
189
+ end
24
190
  end
25
191
  end
26
192
  end
@@ -1,14 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "timeout"
4
+
3
5
  module ReactOnRails
4
6
  module Dev
5
7
  class ProcessManager
8
+ # Timeout for version check operations to prevent hanging
9
+ VERSION_CHECK_TIMEOUT = 5
10
+
6
11
  class << self
12
+ # Check if a process is available and usable in the current execution context
13
+ # This accounts for bundler context where system commands might be intercepted
7
14
  def installed?(process)
8
- IO.popen([process, "-v"], &:close)
9
- true
10
- rescue Errno::ENOENT
11
- false
15
+ installed_in_current_context?(process)
12
16
  end
13
17
 
14
18
  def ensure_procfile(procfile)
@@ -31,20 +35,138 @@ module ReactOnRails
31
35
  # Clean up stale files before starting
32
36
  FileManager.cleanup_stale_files
33
37
 
34
- if installed?("overmind")
35
- system("overmind", "start", "-f", procfile)
36
- elsif installed?("foreman")
37
- system("foreman", "start", "-f", procfile)
38
+ # Try process managers in order of preference
39
+ # run_process_if_available returns:
40
+ # - true/false (exit status) if process was found and executed
41
+ # - nil if process was not found
42
+ overmind_result = run_process_if_available("overmind", ["start", "-f", procfile])
43
+ return if overmind_result == true # Overmind ran successfully
44
+
45
+ foreman_result = run_process_if_available("foreman", ["start", "-f", procfile])
46
+ return if foreman_result == true # Foreman ran successfully
47
+
48
+ # If either process was found but exited with error, don't show "not found" message
49
+ # The process manager itself will have shown its own error message
50
+ exit 1 if overmind_result == false || foreman_result == false
51
+
52
+ # Neither process manager was found
53
+ show_process_manager_installation_help
54
+ exit 1
55
+ end
56
+
57
+ private
58
+
59
+ # Check if a process is actually usable in the current execution context
60
+ # This is important for commands that might be intercepted by bundler
61
+ def installed_in_current_context?(process)
62
+ # Try to execute the process with version flags to see if it works
63
+ # Use system() because that's how we'll actually call it later
64
+ version_flags_for(process).any? do |flag|
65
+ # Add timeout to prevent hanging on version checks
66
+ Timeout.timeout(VERSION_CHECK_TIMEOUT) do
67
+ system(process, flag, out: File::NULL, err: File::NULL)
68
+ end
69
+ end
70
+ rescue Errno::ENOENT, Timeout::Error
71
+ false
72
+ end
73
+
74
+ # Get appropriate version flags for different processes
75
+ def version_flags_for(process)
76
+ case process
77
+ when "overmind"
78
+ ["--version"]
79
+ when "foreman"
80
+ ["--version", "-v"]
38
81
  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
82
+ ["--version", "-v", "-V"]
44
83
  end
45
84
  end
46
85
 
47
- private
86
+ # Try to run a process if it's available, with intelligent fallback strategy
87
+ # Returns:
88
+ # - true if process ran and exited successfully (exit code 0)
89
+ # - false if process ran but exited with error (non-zero exit code)
90
+ # - nil if process was not found/available
91
+ def run_process_if_available(process, args)
92
+ # First attempt: try in current context (works for bundled processes)
93
+ return system(process, *args) if installed?(process)
94
+
95
+ # Second attempt: try in system context (works for system-installed processes)
96
+ return run_process_outside_bundle(process, args) if process_available_in_system?(process)
97
+
98
+ # Process not available in either context
99
+ nil
100
+ end
101
+
102
+ # Run a process outside of bundler context
103
+ # This allows using system-installed processes even when they're not in the Gemfile
104
+ def run_process_outside_bundle(process, args)
105
+ if defined?(Bundler)
106
+ with_unbundled_context do
107
+ system(process, *args)
108
+ end
109
+ else
110
+ # Fallback if Bundler is not available
111
+ system(process, *args)
112
+ end
113
+ end
114
+
115
+ # Check if a process is available system-wide (outside bundle context)
116
+ def process_available_in_system?(process)
117
+ return false unless defined?(Bundler)
118
+
119
+ with_unbundled_context do
120
+ # Try version flags to check if process exists outside bundler context
121
+ version_flags_for(process).any? do |flag|
122
+ # Add timeout to prevent hanging on version checks
123
+ Timeout.timeout(VERSION_CHECK_TIMEOUT) do
124
+ system(process, flag, out: File::NULL, err: File::NULL)
125
+ end
126
+ end
127
+ end
128
+ rescue Errno::ENOENT, Timeout::Error
129
+ false
130
+ end
131
+
132
+ # DRY helper method for Bundler context switching with API compatibility
133
+ # Supports both new (with_unbundled_env) and legacy (with_clean_env) Bundler APIs
134
+ def with_unbundled_context(&block)
135
+ if Bundler.respond_to?(:with_unbundled_env)
136
+ Bundler.with_unbundled_env(&block)
137
+ elsif Bundler.respond_to?(:with_clean_env)
138
+ Bundler.with_clean_env(&block)
139
+ else
140
+ # Fallback if neither method is available (very old Bundler versions)
141
+ yield
142
+ end
143
+ end
144
+
145
+ # Improved error message with helpful guidance
146
+ def show_process_manager_installation_help
147
+ warn <<~MSG
148
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
149
+ ERROR: Process Manager Not Found
150
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
151
+
152
+ To run the React on Rails development server, you need either 'overmind' or
153
+ 'foreman' installed on your system.
154
+
155
+ RECOMMENDED INSTALLATION:
156
+
157
+ macOS: brew install overmind
158
+ Linux: gem install foreman
159
+
160
+ IMPORTANT:
161
+ DO NOT add foreman to your Gemfile. Install it globally on your system.
162
+
163
+ For more information about why foreman should not be in your Gemfile, see:
164
+ https://github.com/ddollar/foreman/wiki/Don't-Bundle-Foreman
165
+
166
+ After installation, try running this script again.
167
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
168
+ MSG
169
+ end
48
170
 
49
171
  def valid_procfile_path?(procfile)
50
172
  # Reject paths with shell metacharacters