react_on_rails 15.0.0.rc.2 → 16.0.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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -36
  3. data/CLAUDE.md +90 -0
  4. data/CODING_AGENTS.md +312 -0
  5. data/CONTRIBUTING.md +378 -3
  6. data/Gemfile.lock +2 -1
  7. data/LICENSE.md +16 -4
  8. data/LICENSES/README.md +14 -0
  9. data/REACT-ON-RAILS-PRO-LICENSE.md +129 -0
  10. data/README.md +2 -2
  11. data/TODO.md +135 -0
  12. data/eslint.config.ts +2 -0
  13. data/lib/generators/USAGE +4 -5
  14. data/lib/generators/react_on_rails/base_generator.rb +263 -57
  15. data/lib/generators/react_on_rails/bin/dev +38 -22
  16. data/lib/generators/react_on_rails/dev_tests_generator.rb +1 -0
  17. data/lib/generators/react_on_rails/generator_helper.rb +31 -1
  18. data/lib/generators/react_on_rails/generator_messages.rb +138 -17
  19. data/lib/generators/react_on_rails/install_generator.rb +222 -20
  20. data/lib/generators/react_on_rails/react_no_redux_generator.rb +6 -5
  21. data/lib/generators/react_on_rails/react_with_redux_generator.rb +37 -13
  22. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev +5 -0
  23. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev-prod-assets +8 -0
  24. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev-static-assets +2 -0
  25. data/lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx +0 -5
  26. data/lib/generators/react_on_rails/templates/base/base/app/javascript/packs/server-bundle.js +1 -8
  27. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.jsx +21 -0
  28. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css +4 -0
  29. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.server.jsx +5 -0
  30. data/lib/generators/react_on_rails/templates/base/base/app/views/hello_world/index.html.erb.tt +1 -1
  31. data/lib/generators/react_on_rails/templates/base/base/app/views/layouts/hello_world.html.erb +4 -2
  32. data/lib/generators/react_on_rails/templates/base/base/babel.config.js.tt +5 -2
  33. data/lib/generators/react_on_rails/templates/base/base/bin/dev +46 -0
  34. data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +3 -3
  35. data/lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml +76 -7
  36. data/lib/generators/react_on_rails/templates/base/base/config/webpack/commonWebpackConfig.js.tt +1 -1
  37. data/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt +8 -8
  38. data/lib/generators/react_on_rails/templates/base/base/config/webpack/production.js.tt +2 -2
  39. data/lib/generators/react_on_rails/templates/base/base/config/webpack/test.js.tt +2 -2
  40. data/lib/generators/react_on_rails/templates/dev_tests/spec/system/hello_world_spec.rb +0 -2
  41. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx +0 -6
  42. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.module.css +4 -0
  43. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.jsx +5 -0
  44. data/lib/react_on_rails/configuration.rb +5 -5
  45. data/lib/react_on_rails/controller.rb +5 -3
  46. data/lib/react_on_rails/dev/file_manager.rb +78 -0
  47. data/lib/react_on_rails/dev/pack_generator.rb +27 -0
  48. data/lib/react_on_rails/dev/process_manager.rb +61 -0
  49. data/lib/react_on_rails/dev/server_manager.rb +330 -0
  50. data/lib/react_on_rails/dev.rb +20 -0
  51. data/lib/react_on_rails/engine.rb +6 -0
  52. data/lib/react_on_rails/git_utils.rb +12 -2
  53. data/lib/react_on_rails/helper.rb +61 -17
  54. data/lib/react_on_rails/packer_utils.rb +4 -18
  55. data/lib/react_on_rails/packs_generator.rb +134 -8
  56. data/lib/react_on_rails/react_component/render_options.rb +2 -2
  57. data/lib/react_on_rails/server_rendering_js_code.rb +0 -1
  58. data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +1 -0
  59. data/lib/react_on_rails/test_helper/webpack_assets_status_checker.rb +1 -0
  60. data/lib/react_on_rails/utils.rb +16 -1
  61. data/lib/react_on_rails/version.rb +1 -1
  62. data/lib/react_on_rails/version_syntax_converter.rb +1 -1
  63. data/lib/react_on_rails.rb +1 -0
  64. data/lib/tasks/generate_packs.rake +20 -0
  65. data/react_on_rails.gemspec +1 -0
  66. metadata +40 -11
  67. data/REACT-ON-RAILS-PRO-LICENSE +0 -95
  68. data/lib/generators/react_on_rails/adapt_for_older_shakapacker_generator.rb +0 -41
  69. data/lib/generators/react_on_rails/bin/dev-static +0 -30
  70. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev-static.tt +0 -9
  71. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev.tt +0 -5
  72. data/lib/generators/react_on_rails/templates/base/base/app/javascript/packs/registration.js.tt +0 -8
  73. /data/lib/generators/react_on_rails/templates/base/base/config/webpack/{webpackConfig.js.tt → generateWebpackConfigs.js.tt} +0 -0
  74. /data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/{HelloWorldApp.jsx → HelloWorldApp.client.jsx} +0 -0
@@ -0,0 +1,330 @@
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)
12
+ case mode
13
+ when :production_like
14
+ run_production_like(_verbose: verbose)
15
+ when :static
16
+ procfile ||= "Procfile.dev-static-assets"
17
+ run_static_development(procfile, verbose: verbose)
18
+ when :development, :hmr
19
+ procfile ||= "Procfile.dev"
20
+ run_development(procfile, verbose: verbose)
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
+ private
120
+
121
+ def help_usage
122
+ Rainbow("📋 Usage: bin/dev [command] [options]").bold
123
+ end
124
+
125
+ # rubocop:disable Metrics/AbcSize
126
+ def help_commands
127
+ <<~COMMANDS
128
+ #{Rainbow('🚀 COMMANDS:').cyan.bold}
129
+ #{Rainbow('(none) / hmr').green.bold} #{Rainbow('Start development server with HMR (default)').white}
130
+ #{Rainbow('→ Uses:').yellow} Procfile.dev
131
+
132
+ #{Rainbow('static').green.bold} #{Rainbow('Start development server with static assets (no HMR, no FOUC)').white}
133
+ #{Rainbow('→ Uses:').yellow} Procfile.dev-static-assets
134
+
135
+ #{Rainbow('production-assets').green.bold} #{Rainbow('Start with production-optimized assets (no HMR)').white}
136
+ #{Rainbow('prod').green.bold} #{Rainbow('Alias for production-assets').white}
137
+ #{Rainbow('→ Uses:').yellow} Procfile.dev-prod-assets
138
+
139
+ #{Rainbow('kill').red.bold} #{Rainbow('Kill all development processes for a clean start').white}
140
+ #{Rainbow('help').blue.bold} #{Rainbow('Show this help message').white}
141
+ COMMANDS
142
+ end
143
+ # rubocop:enable Metrics/AbcSize
144
+
145
+ def help_options
146
+ <<~OPTIONS
147
+ #{Rainbow('⚙️ OPTIONS:').cyan.bold}
148
+ #{Rainbow('--verbose, -v').green.bold} #{Rainbow('Enable verbose output for pack generation').white}
149
+ OPTIONS
150
+ end
151
+
152
+ def help_customization
153
+ <<~CUSTOMIZATION
154
+ #{Rainbow('🔧 CUSTOMIZATION:').cyan.bold}
155
+ Each mode uses a specific Procfile that you can customize for your application:
156
+
157
+ #{Rainbow('•').yellow} #{Rainbow('Procfile.dev').green.bold} - HMR development with webpack-dev-server
158
+ #{Rainbow('•').yellow} #{Rainbow('Procfile.dev-static-assets').green.bold} - Static development with webpack --watch
159
+ #{Rainbow('•').yellow} #{Rainbow('Procfile.dev-prod-assets').green.bold} - Production-optimized assets (port 3001)
160
+
161
+ #{Rainbow('Edit these files to customize the development environment for your needs.').white}
162
+ CUSTOMIZATION
163
+ end
164
+
165
+ # rubocop:disable Metrics/AbcSize
166
+ def help_mode_details
167
+ <<~MODES
168
+ #{Rainbow('🔥 HMR Development mode (default)').cyan.bold} - #{Rainbow('Procfile.dev').green}:
169
+ #{Rainbow('•').yellow} #{Rainbow('Hot Module Replacement (HMR) enabled').white}
170
+ #{Rainbow('•').yellow} #{Rainbow('React on Rails pack generation before Procfile start').white}
171
+ #{Rainbow('•').yellow} #{Rainbow('Webpack dev server for fast recompilation').white}
172
+ #{Rainbow('•').yellow} #{Rainbow('Source maps for debugging').white}
173
+ #{Rainbow('•').yellow} #{Rainbow('May have Flash of Unstyled Content (FOUC)').white}
174
+ #{Rainbow('•').yellow} #{Rainbow('Fast recompilation').white}
175
+ #{Rainbow('•').yellow} #{Rainbow('Access at:').white} #{Rainbow('http://localhost:3000/hello_world').cyan.underline}
176
+
177
+ #{Rainbow('📦 Static development mode').cyan.bold} - #{Rainbow('Procfile.dev-static-assets').green}:
178
+ #{Rainbow('•').yellow} #{Rainbow('No HMR (static assets with auto-recompilation)').white}
179
+ #{Rainbow('•').yellow} #{Rainbow('React on Rails pack generation before Procfile start').white}
180
+ #{Rainbow('•').yellow} #{Rainbow('Webpack watch mode for auto-recompilation').white}
181
+ #{Rainbow('•').yellow} #{Rainbow('CSS extracted to separate files (no FOUC)').white}
182
+ #{Rainbow('•').yellow} #{Rainbow('Development environment (faster builds than production)').white}
183
+ #{Rainbow('•').yellow} #{Rainbow('Source maps for debugging').white}
184
+ #{Rainbow('•').yellow} #{Rainbow('Access at:').white} #{Rainbow('http://localhost:3000/hello_world').cyan.underline}
185
+
186
+ #{Rainbow('🏭 Production-assets mode').cyan.bold} - #{Rainbow('Procfile.dev-prod-assets').green}:
187
+ #{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}
191
+ #{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}
194
+ MODES
195
+ end
196
+ # rubocop:enable Metrics/AbcSize
197
+
198
+ def run_production_like(_verbose: false)
199
+ procfile = "Procfile.dev-prod-assets"
200
+
201
+ print_procfile_info(procfile)
202
+ print_server_info(
203
+ "🏭 Starting production-like development server...",
204
+ [
205
+ "Generating React on Rails packs",
206
+ "Precompiling assets with production optimizations",
207
+ "Running Rails server on port 3001",
208
+ "No HMR (Hot Module Replacement)",
209
+ "CSS extracted to separate files (no FOUC)"
210
+ ],
211
+ 3001
212
+ )
213
+
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"
217
+
218
+ if success
219
+ puts "✅ Assets precompiled successfully"
220
+ ProcessManager.ensure_procfile(procfile)
221
+ ProcessManager.run_with_process_manager(procfile)
222
+ else
223
+ puts "❌ Asset precompilation failed"
224
+ exit 1
225
+ end
226
+ end
227
+
228
+ def run_static_development(procfile, verbose: false)
229
+ print_procfile_info(procfile)
230
+ print_server_info(
231
+ "⚡ Starting development server with static assets...",
232
+ [
233
+ "Generating React on Rails packs",
234
+ "Using shakapacker --watch (no HMR)",
235
+ "CSS extracted to separate files (no FOUC)",
236
+ "Development environment (source maps, faster builds)",
237
+ "Auto-recompiles on file changes"
238
+ ]
239
+ )
240
+
241
+ PackGenerator.generate(verbose: verbose)
242
+ ProcessManager.ensure_procfile(procfile)
243
+ ProcessManager.run_with_process_manager(procfile)
244
+ end
245
+
246
+ def run_development(procfile, verbose: false)
247
+ print_procfile_info(procfile)
248
+ PackGenerator.generate(verbose: verbose)
249
+ ProcessManager.ensure_procfile(procfile)
250
+ ProcessManager.run_with_process_manager(procfile)
251
+ end
252
+
253
+ def print_server_info(title, features, port = 3000)
254
+ puts title
255
+ features.each { |feature| puts " - #{feature}" }
256
+ puts ""
257
+ puts ""
258
+ puts "💡 Access at: #{Rainbow("http://localhost:#{port}/hello_world").cyan.underline}"
259
+ puts ""
260
+ end
261
+
262
+ def print_procfile_info(procfile)
263
+ port = procfile_port(procfile)
264
+ box_width = 60
265
+
266
+ puts ""
267
+ puts box_border(box_width)
268
+ puts box_empty_line(box_width)
269
+ puts format_box_line("📋 Using Procfile: #{procfile}", box_width)
270
+ puts format_box_line("🔧 Customize this file for your app's needs", box_width)
271
+ puts box_empty_line(box_width)
272
+ puts format_box_line("💡 Access at: #{Rainbow("http://localhost:#{port}/hello_world").cyan.underline}",
273
+ box_width)
274
+ puts box_empty_line(box_width)
275
+ puts box_bottom(box_width)
276
+ puts ""
277
+ end
278
+
279
+ def procfile_port(procfile)
280
+ procfile == "Procfile.dev-prod-assets" ? 3001 : 3000
281
+ end
282
+
283
+ def box_border(width)
284
+ "┌#{'─' * (width - 2)}┐"
285
+ end
286
+
287
+ def box_bottom(width)
288
+ "└#{'─' * (width - 2)}┘"
289
+ end
290
+
291
+ def box_empty_line(width)
292
+ "│#{' ' * (width - 2)}│"
293
+ end
294
+
295
+ def format_box_line(content, box_width)
296
+ line = "│ #{content}"
297
+ # Use visual length for colored text
298
+ visual_length = Rainbow.uncolor(line).length
299
+ padding = box_width - visual_length - 2
300
+ line + "#{' ' * padding}│"
301
+ end
302
+
303
+ # rubocop:disable Metrics/AbcSize
304
+ def help_troubleshooting
305
+ <<~TROUBLESHOOTING
306
+ #{Rainbow('🔧 TROUBLESHOOTING:').cyan.bold}
307
+
308
+ #{Rainbow('⚛️ React Refresh Issues:').yellow.bold}
309
+ #{Rainbow('If you see "$RefreshSig$ is not defined" errors:').white}
310
+ #{Rainbow('1.').green} #{Rainbow('Check that both babel plugin and webpack plugin are configured:').white}
311
+ #{Rainbow('•').yellow} #{Rainbow('babel.config.js: \'react-refresh/babel\' plugin (enabled when WEBPACK_SERVE=true)').white}
312
+ #{Rainbow('•').yellow} #{Rainbow('config/webpack/development.js: ReactRefreshWebpackPlugin (enabled when WEBPACK_SERVE=true)').white}
313
+ #{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}
314
+ #{Rainbow('3.').green} #{Rainbow('Try restarting the development server:').white} #{Rainbow('bin/dev kill && bin/dev').green.bold}
315
+ #{Rainbow('4.').green} #{Rainbow('Note: React Refresh only works in HMR mode, not static mode').white}
316
+
317
+ #{Rainbow('🚨 General Issues:').yellow.bold}
318
+ #{Rainbow('•').red} #{Rainbow('"Port already in use"').white} #{Rainbow('→ Run:').yellow} #{Rainbow('bin/dev kill').green.bold}
319
+ #{Rainbow('•').red} #{Rainbow('"Webpack compilation failed"').white} #{Rainbow('→ Check console for specific errors').white}
320
+ #{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}
321
+ #{Rainbow('•').red} #{Rainbow('"Assets not loading"').white} #{Rainbow('→ Verify Procfile.dev is present and check server logs').white}
322
+
323
+ #{Rainbow('📚 Need help? Visit:').blue.bold} #{Rainbow('https://www.shakacode.com/react-on-rails/docs/').cyan.underline}
324
+ TROUBLESHOOTING
325
+ end
326
+ # rubocop:enable Metrics/AbcSize
327
+ end
328
+ end
329
+ end
330
+ 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
@@ -8,5 +8,11 @@ module ReactOnRails
8
8
  VersionChecker.build.log_if_gem_and_node_package_versions_differ
9
9
  ReactOnRails::ServerRenderingPool.reset_pool
10
10
  end
11
+
12
+ rake_tasks do
13
+ load File.expand_path("../tasks/generate_packs.rake", __dir__)
14
+ load File.expand_path("../tasks/assets.rake", __dir__)
15
+ load File.expand_path("../tasks/locale.rake", __dir__)
16
+ end
11
17
  end
12
18
  end
@@ -11,9 +11,19 @@ module ReactOnRails
11
11
  return false if git_installed && status&.empty?
12
12
 
13
13
  error = if git_installed
14
- "You have uncommitted code. Please commit or stash your changes before continuing"
14
+ <<~MSG.strip
15
+ You have uncommitted changes. Please commit or stash them before continuing.
16
+
17
+ The React on Rails generator creates many new files and it's important to keep
18
+ your existing changes separate from the generated code for easier review.
19
+ MSG
15
20
  else
16
- "You do not have Git installed. Please install Git, and commit your changes before continuing"
21
+ <<~MSG.strip
22
+ Git is not installed. Please install Git and commit your changes before continuing.
23
+
24
+ The React on Rails generator creates many new files and version control helps
25
+ track what was generated versus your existing code.
26
+ MSG
17
27
  end
18
28
  message_handler.add_error(error)
19
29
  true
@@ -17,6 +17,9 @@ module ReactOnRails
17
17
  include ReactOnRails::Utils::Required
18
18
 
19
19
  COMPONENT_HTML_KEY = "componentHtml"
20
+ IMMEDIATE_HYDRATION_PRO_WARNING = "[REACT ON RAILS] The 'immediate_hydration' feature requires a " \
21
+ "React on Rails Pro license. " \
22
+ "Please visit https://shakacode.com/react-on-rails-pro to learn more."
20
23
 
21
24
  # react_component_name: can be a React function or class component or a "Render-Function".
22
25
  # "Render-Functions" differ from a React function in that they take two parameters, the
@@ -58,15 +61,17 @@ module ReactOnRails
58
61
  server_rendered_html = internal_result[:result]["html"]
59
62
  console_script = internal_result[:result]["consoleReplayScript"]
60
63
  render_options = internal_result[:render_options]
64
+ badge = pro_warning_badge_if_needed(internal_result[:immediate_hydration_requested])
61
65
 
62
66
  case server_rendered_html
63
67
  when String
64
- build_react_component_result_for_server_rendered_string(
68
+ html = build_react_component_result_for_server_rendered_string(
65
69
  server_rendered_html: server_rendered_html,
66
70
  component_specification_tag: internal_result[:tag],
67
71
  console_script: console_script,
68
72
  render_options: render_options
69
73
  )
74
+ (badge + html).html_safe
70
75
  when Hash
71
76
  msg = <<~MSG
72
77
  Use react_component_hash (not react_component) to return a Hash to your ruby view code. See
@@ -126,7 +131,7 @@ module ReactOnRails
126
131
  # stream_react_component doesn't have the prerender option
127
132
  # Because setting prerender to false is equivalent to calling react_component with prerender: false
128
133
  options[:prerender] = true
129
- options = options.merge(force_load: true) unless options.key?(:force_load)
134
+ options = options.merge(immediate_hydration: true) unless options.key?(:immediate_hydration)
130
135
  run_stream_inside_fiber do
131
136
  internal_stream_react_component(component_name, options)
132
137
  end
@@ -208,22 +213,26 @@ module ReactOnRails
208
213
  #
209
214
  def react_component_hash(component_name, options = {})
210
215
  options[:prerender] = true
216
+
211
217
  internal_result = internal_react_component(component_name, options)
212
218
  server_rendered_html = internal_result[:result]["html"]
213
219
  console_script = internal_result[:result]["consoleReplayScript"]
214
220
  render_options = internal_result[:render_options]
221
+ badge = pro_warning_badge_if_needed(internal_result[:immediate_hydration_requested])
215
222
 
216
223
  if server_rendered_html.is_a?(String) && internal_result[:result]["hasErrors"]
217
224
  server_rendered_html = { COMPONENT_HTML_KEY => internal_result[:result]["html"] }
218
225
  end
219
226
 
220
227
  if server_rendered_html.is_a?(Hash)
221
- build_react_component_result_for_server_rendered_hash(
228
+ result = build_react_component_result_for_server_rendered_hash(
222
229
  server_rendered_html: server_rendered_html,
223
230
  component_specification_tag: internal_result[:tag],
224
231
  console_script: console_script,
225
232
  render_options: render_options
226
233
  )
234
+ result[COMPONENT_HTML_KEY] = badge + result[COMPONENT_HTML_KEY]
235
+ result
227
236
  else
228
237
  msg = <<~MSG
229
238
  Render-Function used by react_component_hash for #{component_name} is expected to return
@@ -247,13 +256,16 @@ module ReactOnRails
247
256
  # props: Ruby Hash or JSON string which contains the properties to pass to the redux store.
248
257
  # Options
249
258
  # defer: false -- pass as true if you wish to render this below your component.
250
- # force_load: false -- pass as true if you wish to hydrate this store immediately instead of
251
- # waiting for the page to load.
252
- def redux_store(store_name, props: {}, defer: false, force_load: nil)
253
- force_load = ReactOnRails.configuration.force_load if force_load.nil?
259
+ # immediate_hydration: false -- React on Rails Pro (licensed) feature. Pass as true if you wish to
260
+ # hydrate this store immediately instead of waiting for the page to load.
261
+ def redux_store(store_name, props: {}, defer: false, immediate_hydration: nil)
262
+ immediate_hydration = ReactOnRails.configuration.immediate_hydration if immediate_hydration.nil?
263
+ badge = pro_warning_badge_if_needed(immediate_hydration)
264
+ immediate_hydration = false unless support_pro_features?
265
+
254
266
  redux_store_data = { store_name: store_name,
255
267
  props: props,
256
- force_load: force_load }
268
+ immediate_hydration: immediate_hydration }
257
269
  if defer
258
270
  registered_stores_defer_render << redux_store_data
259
271
  "YOU SHOULD NOT SEE THIS ON YOUR VIEW -- Uses as a code block, like <% redux_store %> " \
@@ -261,7 +273,7 @@ module ReactOnRails
261
273
  else
262
274
  registered_stores << redux_store_data
263
275
  result = render_redux_store_data(redux_store_data)
264
- prepend_render_rails_context(result)
276
+ (badge + prepend_render_rails_context(result)).html_safe
265
277
  end
266
278
  end
267
279
 
@@ -334,7 +346,7 @@ module ReactOnRails
334
346
 
335
347
  html = result["html"]
336
348
  console_log_script = result["consoleLogScript"]
337
- raw("#{html}#{render_options.replay_console ? console_log_script : ''}")
349
+ raw("#{html}#{console_log_script if render_options.replay_console}")
338
350
  rescue ExecJS::ProgramError => err
339
351
  raise ReactOnRails::PrerenderError.new(component_name: "N/A (server_render_js called)",
340
352
  err: err,
@@ -401,7 +413,7 @@ module ReactOnRails
401
413
  result.merge!(
402
414
  # URL settings
403
415
  href: uri.to_s,
404
- location: "#{uri.path}#{uri.query.present? ? "?#{uri.query}" : ''}",
416
+ location: "#{uri.path}#{"?#{uri.query}" if uri.query.present?}",
405
417
  scheme: uri.scheme, # http
406
418
  host: uri.host, # foo.com
407
419
  port: uri.port,
@@ -440,7 +452,32 @@ module ReactOnRails
440
452
 
441
453
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
442
454
 
443
- private
455
+ # Checks if React on Rails Pro features are available
456
+ # @return [Boolean] true if Pro license is valid, false otherwise
457
+ def support_pro_features?
458
+ ReactOnRails::Utils.react_on_rails_pro_licence_valid?
459
+ end
460
+
461
+ def pro_warning_badge_if_needed(immediate_hydration)
462
+ return "".html_safe unless immediate_hydration
463
+ return "".html_safe if support_pro_features?
464
+
465
+ puts IMMEDIATE_HYDRATION_PRO_WARNING
466
+ Rails.logger.warn IMMEDIATE_HYDRATION_PRO_WARNING
467
+
468
+ tooltip_text = "The 'immediate_hydration' feature requires a React on Rails Pro license. Click to learn more."
469
+
470
+ badge_html = <<~HTML
471
+ <a href="https://shakacode.com/react-on-rails-pro" target="_blank" rel="noopener noreferrer" title="#{tooltip_text}">
472
+ <div style="position: fixed; top: 0; right: 0; width: 180px; height: 180px; overflow: hidden; z-index: 9999; pointer-events: none;">
473
+ <div style="position: absolute; top: 50px; right: -40px; transform: rotate(45deg); background-color: rgba(220, 53, 69, 0.85); color: white; padding: 7px 40px; text-align: center; font-weight: bold; font-family: sans-serif; font-size: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.3); pointer-events: auto;">
474
+ React On Rails Pro Required
475
+ </div>
476
+ </div>
477
+ </a>
478
+ HTML
479
+ badge_html.strip.html_safe
480
+ end
444
481
 
445
482
  def run_stream_inside_fiber
446
483
  unless ReactOnRails::Utils.react_on_rails_pro?
@@ -638,6 +675,9 @@ module ReactOnRails
638
675
  # server has already rendered the HTML.
639
676
 
640
677
  render_options = create_render_options(react_component_name, options)
678
+ # Capture the originally requested value so we can show a badge while still disabling the feature.
679
+ immediate_hydration_requested = render_options.immediate_hydration
680
+ render_options.set_option(:immediate_hydration, false) unless support_pro_features?
641
681
 
642
682
  # Setup the page_loaded_js, which is the same regardless of prerendering or not!
643
683
  # The reason is that React is smart about not doing extra work if the server rendering did its job.
@@ -650,9 +690,10 @@ module ReactOnRails
650
690
  "data-trace" => (render_options.trace ? true : nil),
651
691
  "data-dom-id" => render_options.dom_id,
652
692
  "data-store-dependencies" => render_options.store_dependencies&.to_json,
653
- "data-force-load" => (render_options.force_load ? true : nil))
693
+ "data-immediate-hydration" =>
694
+ (render_options.immediate_hydration ? true : nil))
654
695
 
655
- if render_options.force_load
696
+ if render_options.immediate_hydration
656
697
  component_specification_tag.concat(
657
698
  content_tag(:script, %(
658
699
  typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsComponentLoaded('#{render_options.dom_id}');
@@ -667,7 +708,8 @@ typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsComponentLoaded('#{
667
708
  {
668
709
  render_options: render_options,
669
710
  tag: component_specification_tag,
670
- result: result
711
+ result: result,
712
+ immediate_hydration_requested: immediate_hydration_requested
671
713
  }
672
714
  end
673
715
 
@@ -676,9 +718,10 @@ typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsComponentLoaded('#{
676
718
  json_safe_and_pretty(redux_store_data[:props]).html_safe,
677
719
  type: "application/json",
678
720
  "data-js-react-on-rails-store" => redux_store_data[:store_name].html_safe,
679
- "data-force-load" => (redux_store_data[:force_load] ? true : nil))
721
+ "data-immediate-hydration" =>
722
+ (redux_store_data[:immediate_hydration] ? true : nil))
680
723
 
681
- if redux_store_data[:force_load]
724
+ if redux_store_data[:immediate_hydration]
682
725
  store_hydration_data.concat(
683
726
  content_tag(:script, <<~JS.strip_heredoc.html_safe
684
727
  typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsStoreLoaded('#{redux_store_data[:store_name]}');
@@ -814,6 +857,7 @@ typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsComponentLoaded('#{
814
857
 
815
858
  if defined?(ScoutApm)
816
859
  include ScoutApm::Tracer
860
+
817
861
  instrument_method :react_component, type: "ReactOnRails", name: "react_component"
818
862
  instrument_method :react_component_hash, type: "ReactOnRails", name: "react_component_hash"
819
863
  end
@@ -3,27 +3,18 @@
3
3
  module ReactOnRails
4
4
  module PackerUtils
5
5
  def self.using_packer?
6
- using_shakapacker_const? || using_webpacker_const?
6
+ using_shakapacker_const?
7
7
  end
8
8
 
9
9
  def self.using_shakapacker_const?
10
10
  return @using_shakapacker_const if defined?(@using_shakapacker_const)
11
11
 
12
12
  @using_shakapacker_const = ReactOnRails::Utils.gem_available?("shakapacker") &&
13
- shakapacker_version_requirement_met?("7.0.0")
14
- end
15
-
16
- def self.using_webpacker_const?
17
- return @using_webpacker_const if defined?(@using_webpacker_const)
18
-
19
- @using_webpacker_const = (ReactOnRails::Utils.gem_available?("shakapacker") &&
20
- shakapacker_version_as_array[0] <= 6) ||
21
- ReactOnRails::Utils.gem_available?("webpacker")
13
+ shakapacker_version_requirement_met?("8.2.0")
22
14
  end
23
15
 
24
16
  def self.packer_type
25
17
  return "shakapacker" if using_shakapacker_const?
26
- return "webpacker" if using_webpacker_const?
27
18
 
28
19
  nil
29
20
  end
@@ -31,12 +22,8 @@ module ReactOnRails
31
22
  def self.packer
32
23
  return nil unless using_packer?
33
24
 
34
- if using_shakapacker_const?
35
- require "shakapacker"
36
- return ::Shakapacker
37
- end
38
- require "webpacker"
39
- ::Webpacker
25
+ require "shakapacker"
26
+ ::Shakapacker
40
27
  end
41
28
 
42
29
  def self.dev_server_running?
@@ -106,7 +93,6 @@ module ReactOnRails
106
93
  end
107
94
 
108
95
  def self.precompile?
109
- return ::Webpacker.config.webpacker_precompile? if using_webpacker_const?
110
96
  return ::Shakapacker.config.shakapacker_precompile? if using_shakapacker_const?
111
97
 
112
98
  false