react_on_rails 15.0.0.rc.2 → 16.0.1.rc.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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +103 -34
  3. data/CLAUDE.md +102 -0
  4. data/CODING_AGENTS.md +312 -0
  5. data/CONTRIBUTING.md +378 -3
  6. data/Gemfile.lock +2 -1
  7. data/LICENSE.md +30 -4
  8. data/LICENSES/README.md +14 -0
  9. data/REACT-ON-RAILS-PRO-LICENSE.md +129 -0
  10. data/README.md +70 -20
  11. data/TODO.md +135 -0
  12. data/eslint.config.ts +5 -0
  13. data/knip.ts +20 -9
  14. data/lib/generators/USAGE +4 -5
  15. data/lib/generators/react_on_rails/USAGE +65 -0
  16. data/lib/generators/react_on_rails/base_generator.rb +263 -57
  17. data/lib/generators/react_on_rails/dev_tests_generator.rb +1 -0
  18. data/lib/generators/react_on_rails/generator_helper.rb +35 -1
  19. data/lib/generators/react_on_rails/generator_messages.rb +138 -17
  20. data/lib/generators/react_on_rails/install_generator.rb +336 -26
  21. data/lib/generators/react_on_rails/react_no_redux_generator.rb +19 -6
  22. data/lib/generators/react_on_rails/react_with_redux_generator.rb +111 -18
  23. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev +5 -0
  24. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev-prod-assets +8 -0
  25. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev-static-assets +2 -0
  26. data/lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx +0 -5
  27. data/lib/generators/react_on_rails/templates/base/base/app/javascript/packs/server-bundle.js +1 -8
  28. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.jsx +21 -0
  29. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.tsx +25 -0
  30. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css +4 -0
  31. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.server.jsx +5 -0
  32. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.server.tsx +5 -0
  33. data/lib/generators/react_on_rails/templates/base/base/app/views/hello_world/index.html.erb.tt +1 -1
  34. data/lib/generators/react_on_rails/templates/base/base/app/views/layouts/hello_world.html.erb +4 -2
  35. data/lib/generators/react_on_rails/templates/base/base/babel.config.js.tt +5 -2
  36. data/lib/generators/react_on_rails/templates/base/base/bin/dev +34 -0
  37. data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +3 -3
  38. data/lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml +76 -7
  39. data/lib/generators/react_on_rails/templates/base/base/config/webpack/commonWebpackConfig.js.tt +1 -1
  40. data/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt +8 -8
  41. data/lib/generators/react_on_rails/templates/base/base/config/webpack/production.js.tt +2 -2
  42. data/lib/generators/react_on_rails/templates/base/base/config/webpack/test.js.tt +2 -2
  43. data/lib/generators/react_on_rails/templates/dev_tests/spec/system/hello_world_spec.rb +0 -2
  44. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/actions/helloWorldActionCreators.ts +18 -0
  45. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx +0 -6
  46. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.module.css +4 -0
  47. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.tsx +24 -0
  48. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/constants/helloWorldConstants.ts +6 -0
  49. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/containers/HelloWorldContainer.ts +20 -0
  50. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/reducers/helloWorldReducer.ts +22 -0
  51. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.client.tsx +23 -0
  52. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.jsx +5 -0
  53. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.tsx +5 -0
  54. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/store/helloWorldStore.ts +18 -0
  55. data/lib/react_on_rails/configuration.rb +15 -11
  56. data/lib/react_on_rails/controller.rb +5 -3
  57. data/lib/react_on_rails/dev/file_manager.rb +78 -0
  58. data/lib/react_on_rails/dev/pack_generator.rb +27 -0
  59. data/lib/react_on_rails/dev/process_manager.rb +61 -0
  60. data/lib/react_on_rails/dev/server_manager.rb +487 -0
  61. data/lib/react_on_rails/dev.rb +20 -0
  62. data/lib/react_on_rails/doctor.rb +1149 -0
  63. data/lib/react_on_rails/engine.rb +6 -0
  64. data/lib/react_on_rails/git_utils.rb +12 -2
  65. data/lib/react_on_rails/helper.rb +19 -44
  66. data/lib/react_on_rails/packer_utils.rb +4 -18
  67. data/lib/react_on_rails/packs_generator.rb +134 -8
  68. data/lib/react_on_rails/pro/NOTICE +21 -0
  69. data/lib/react_on_rails/pro/helper.rb +122 -0
  70. data/lib/react_on_rails/pro/utils.rb +53 -0
  71. data/lib/react_on_rails/react_component/render_options.rb +8 -4
  72. data/lib/react_on_rails/server_rendering_js_code.rb +0 -1
  73. data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +1 -0
  74. data/lib/react_on_rails/system_checker.rb +659 -0
  75. data/lib/react_on_rails/test_helper/webpack_assets_status_checker.rb +1 -0
  76. data/lib/react_on_rails/utils.rb +16 -1
  77. data/lib/react_on_rails/version.rb +1 -1
  78. data/lib/react_on_rails/version_syntax_converter.rb +1 -1
  79. data/lib/react_on_rails.rb +1 -0
  80. data/lib/tasks/doctor.rake +51 -0
  81. data/lib/tasks/generate_packs.rake +144 -1
  82. data/package-lock.json +11984 -0
  83. data/react_on_rails.gemspec +1 -0
  84. metadata +55 -11
  85. data/REACT-ON-RAILS-PRO-LICENSE +0 -95
  86. data/lib/generators/react_on_rails/adapt_for_older_shakapacker_generator.rb +0 -41
  87. data/lib/generators/react_on_rails/bin/dev +0 -30
  88. data/lib/generators/react_on_rails/bin/dev-static +0 -30
  89. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev-static.tt +0 -9
  90. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev.tt +0 -5
  91. data/lib/generators/react_on_rails/templates/base/base/app/javascript/packs/registration.js.tt +0 -8
  92. /data/lib/generators/react_on_rails/templates/base/base/config/webpack/{webpackConfig.js.tt → generateWebpackConfigs.js.tt} +0 -0
  93. /data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/{HelloWorldApp.jsx → HelloWorldApp.client.jsx} +0 -0
@@ -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
@@ -11,10 +11,12 @@ require "addressable/uri"
11
11
  require "react_on_rails/utils"
12
12
  require "react_on_rails/json_output"
13
13
  require "active_support/concern"
14
+ require "react_on_rails/pro/helper"
14
15
 
15
16
  module ReactOnRails
16
17
  module Helper
17
18
  include ReactOnRails::Utils::Required
19
+ include ReactOnRails::Pro::Helper
18
20
 
19
21
  COMPONENT_HTML_KEY = "componentHtml"
20
22
 
@@ -61,12 +63,13 @@ module ReactOnRails
61
63
 
62
64
  case server_rendered_html
63
65
  when String
64
- build_react_component_result_for_server_rendered_string(
66
+ html = build_react_component_result_for_server_rendered_string(
65
67
  server_rendered_html: server_rendered_html,
66
68
  component_specification_tag: internal_result[:tag],
67
69
  console_script: console_script,
68
70
  render_options: render_options
69
71
  )
72
+ html.html_safe
70
73
  when Hash
71
74
  msg = <<~MSG
72
75
  Use react_component_hash (not react_component) to return a Hash to your ruby view code. See
@@ -126,7 +129,7 @@ module ReactOnRails
126
129
  # stream_react_component doesn't have the prerender option
127
130
  # Because setting prerender to false is equivalent to calling react_component with prerender: false
128
131
  options[:prerender] = true
129
- options = options.merge(force_load: true) unless options.key?(:force_load)
132
+ options = options.merge(immediate_hydration: true) unless options.key?(:immediate_hydration)
130
133
  run_stream_inside_fiber do
131
134
  internal_stream_react_component(component_name, options)
132
135
  end
@@ -208,6 +211,7 @@ module ReactOnRails
208
211
  #
209
212
  def react_component_hash(component_name, options = {})
210
213
  options[:prerender] = true
214
+
211
215
  internal_result = internal_react_component(component_name, options)
212
216
  server_rendered_html = internal_result[:result]["html"]
213
217
  console_script = internal_result[:result]["consoleReplayScript"]
@@ -224,6 +228,7 @@ module ReactOnRails
224
228
  console_script: console_script,
225
229
  render_options: render_options
226
230
  )
231
+
227
232
  else
228
233
  msg = <<~MSG
229
234
  Render-Function used by react_component_hash for #{component_name} is expected to return
@@ -247,13 +252,14 @@ module ReactOnRails
247
252
  # props: Ruby Hash or JSON string which contains the properties to pass to the redux store.
248
253
  # Options
249
254
  # 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?
255
+ # immediate_hydration: false -- React on Rails Pro (licensed) feature. Pass as true if you wish to
256
+ # hydrate this store immediately instead of waiting for the page to load.
257
+ def redux_store(store_name, props: {}, defer: false, immediate_hydration: nil)
258
+ immediate_hydration = ReactOnRails.configuration.immediate_hydration if immediate_hydration.nil?
259
+
254
260
  redux_store_data = { store_name: store_name,
255
261
  props: props,
256
- force_load: force_load }
262
+ immediate_hydration: immediate_hydration }
257
263
  if defer
258
264
  registered_stores_defer_render << redux_store_data
259
265
  "YOU SHOULD NOT SEE THIS ON YOUR VIEW -- Uses as a code block, like <% redux_store %> " \
@@ -261,7 +267,7 @@ module ReactOnRails
261
267
  else
262
268
  registered_stores << redux_store_data
263
269
  result = render_redux_store_data(redux_store_data)
264
- prepend_render_rails_context(result)
270
+ prepend_render_rails_context(result).html_safe
265
271
  end
266
272
  end
267
273
 
@@ -334,7 +340,7 @@ module ReactOnRails
334
340
 
335
341
  html = result["html"]
336
342
  console_log_script = result["consoleLogScript"]
337
- raw("#{html}#{render_options.replay_console ? console_log_script : ''}")
343
+ raw("#{html}#{console_log_script if render_options.replay_console}")
338
344
  rescue ExecJS::ProgramError => err
339
345
  raise ReactOnRails::PrerenderError.new(component_name: "N/A (server_render_js called)",
340
346
  err: err,
@@ -401,7 +407,7 @@ module ReactOnRails
401
407
  result.merge!(
402
408
  # URL settings
403
409
  href: uri.to_s,
404
- location: "#{uri.path}#{uri.query.present? ? "?#{uri.query}" : ''}",
410
+ location: "#{uri.path}#{"?#{uri.query}" if uri.query.present?}",
405
411
  scheme: uri.scheme, # http
406
412
  host: uri.host, # foo.com
407
413
  port: uri.port,
@@ -440,8 +446,6 @@ module ReactOnRails
440
446
 
441
447
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
442
448
 
443
- private
444
-
445
449
  def run_stream_inside_fiber
446
450
  unless ReactOnRails::Utils.react_on_rails_pro?
447
451
  raise ReactOnRails::Error,
@@ -641,24 +645,7 @@ module ReactOnRails
641
645
 
642
646
  # Setup the page_loaded_js, which is the same regardless of prerendering or not!
643
647
  # The reason is that React is smart about not doing extra work if the server rendering did its job.
644
- component_specification_tag = content_tag(:script,
645
- json_safe_and_pretty(render_options.client_props).html_safe,
646
- type: "application/json",
647
- class: "js-react-on-rails-component",
648
- id: "js-react-on-rails-component-#{render_options.dom_id}",
649
- "data-component-name" => render_options.react_component_name,
650
- "data-trace" => (render_options.trace ? true : nil),
651
- "data-dom-id" => render_options.dom_id,
652
- "data-store-dependencies" => render_options.store_dependencies&.to_json,
653
- "data-force-load" => (render_options.force_load ? true : nil))
654
-
655
- if render_options.force_load
656
- component_specification_tag.concat(
657
- content_tag(:script, %(
658
- typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsComponentLoaded('#{render_options.dom_id}');
659
- ).html_safe)
660
- )
661
- end
648
+ component_specification_tag = generate_component_script(render_options)
662
649
 
663
650
  load_pack_for_generated_component(react_component_name, render_options)
664
651
  # Create the HTML rendering part
@@ -672,20 +659,7 @@ typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsComponentLoaded('#{
672
659
  end
673
660
 
674
661
  def render_redux_store_data(redux_store_data)
675
- store_hydration_data = content_tag(:script,
676
- json_safe_and_pretty(redux_store_data[:props]).html_safe,
677
- type: "application/json",
678
- "data-js-react-on-rails-store" => redux_store_data[:store_name].html_safe,
679
- "data-force-load" => (redux_store_data[:force_load] ? true : nil))
680
-
681
- if redux_store_data[:force_load]
682
- store_hydration_data.concat(
683
- content_tag(:script, <<~JS.strip_heredoc.html_safe
684
- typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsStoreLoaded('#{redux_store_data[:store_name]}');
685
- JS
686
- )
687
- )
688
- end
662
+ store_hydration_data = generate_store_script(redux_store_data)
689
663
 
690
664
  prepend_render_rails_context(store_hydration_data)
691
665
  end
@@ -814,6 +788,7 @@ typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsComponentLoaded('#{
814
788
 
815
789
  if defined?(ScoutApm)
816
790
  include ScoutApm::Tracer
791
+
817
792
  instrument_method :react_component, type: "ReactOnRails", name: "react_component"
818
793
  instrument_method :react_component_hash, type: "ReactOnRails", name: "react_component_hash"
819
794
  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
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fileutils"
4
+ require "set"
4
5
 
5
6
  module ReactOnRails
6
7
  # rubocop:disable Metrics/ClassLength
7
8
  class PacksGenerator
8
9
  CONTAINS_CLIENT_OR_SERVER_REGEX = /\.(server|client)($|\.)/
10
+ COMPONENT_EXTENSIONS = /\.(jsx?|tsx?)$/
9
11
  MINIMUM_SHAKAPACKER_VERSION = "6.5.1"
10
12
 
11
13
  def self.instance
@@ -16,13 +18,20 @@ module ReactOnRails
16
18
  return unless ReactOnRails.configuration.auto_load_bundle
17
19
 
18
20
  add_generated_pack_to_server_bundle
21
+
22
+ # Clean any non-generated files from directories
23
+ clean_non_generated_files_with_feedback
24
+
19
25
  are_generated_files_present_and_up_to_date = Dir.exist?(generated_packs_directory_path) &&
20
26
  File.exist?(generated_server_bundle_file_path) &&
21
27
  !stale_or_missing_packs?
22
28
 
23
- return if are_generated_files_present_and_up_to_date
29
+ if are_generated_files_present_and_up_to_date
30
+ puts Rainbow("✅ Generated packs are up to date, no regeneration needed").green
31
+ return
32
+ end
24
33
 
25
- clean_generated_packs_directory
34
+ clean_generated_directories_with_feedback
26
35
  generate_packs
27
36
  end
28
37
 
@@ -182,9 +191,112 @@ module ReactOnRails
182
191
  "#{generated_nonentrypoints_path}/#{generated_server_bundle_file_name}.js"
183
192
  end
184
193
 
185
- def clean_generated_packs_directory
186
- FileUtils.rm_rf(generated_packs_directory_path)
187
- FileUtils.mkdir_p(generated_packs_directory_path)
194
+ def clean_non_generated_files_with_feedback
195
+ directories_to_clean = [generated_packs_directory_path, generated_server_bundle_directory_path].compact.uniq
196
+ expected_files = build_expected_files_set
197
+
198
+ puts Rainbow("🧹 Cleaning non-generated files...").yellow
199
+
200
+ total_deleted = directories_to_clean.sum do |dir_path|
201
+ clean_unexpected_files_from_directory(dir_path, expected_files)
202
+ end
203
+
204
+ display_cleanup_summary(total_deleted)
205
+ end
206
+
207
+ def build_expected_files_set
208
+ expected_pack_files = Set.new
209
+ common_component_to_path.each_value { |path| expected_pack_files << generated_pack_path(path) }
210
+ client_component_to_path.each_value { |path| expected_pack_files << generated_pack_path(path) }
211
+
212
+ if ReactOnRails.configuration.server_bundle_js_file.present?
213
+ expected_server_bundle = generated_server_bundle_file_path
214
+ end
215
+
216
+ { pack_files: expected_pack_files, server_bundle: expected_server_bundle }
217
+ end
218
+
219
+ def clean_unexpected_files_from_directory(dir_path, expected_files)
220
+ return 0 unless Dir.exist?(dir_path)
221
+
222
+ existing_files = Dir.glob("#{dir_path}/**/*").select { |f| File.file?(f) }
223
+ unexpected_files = find_unexpected_files(existing_files, dir_path, expected_files)
224
+
225
+ if unexpected_files.any?
226
+ delete_unexpected_files(unexpected_files, dir_path)
227
+ unexpected_files.length
228
+ else
229
+ puts Rainbow(" No unexpected files found in #{dir_path}").cyan
230
+ 0
231
+ end
232
+ end
233
+
234
+ def find_unexpected_files(existing_files, dir_path, expected_files)
235
+ existing_files.reject do |file|
236
+ if dir_path == generated_server_bundle_directory_path
237
+ file == expected_files[:server_bundle]
238
+ else
239
+ expected_files[:pack_files].include?(file)
240
+ end
241
+ end
242
+ end
243
+
244
+ def delete_unexpected_files(unexpected_files, dir_path)
245
+ puts Rainbow(" Deleting #{unexpected_files.length} unexpected files from #{dir_path}:").cyan
246
+ unexpected_files.each do |file|
247
+ puts Rainbow(" - #{File.basename(file)}").blue
248
+ File.delete(file)
249
+ end
250
+ end
251
+
252
+ def display_cleanup_summary(total_deleted)
253
+ if total_deleted.positive?
254
+ puts Rainbow("🗑️ Deleted #{total_deleted} unexpected files total").red
255
+ else
256
+ puts Rainbow("✨ No unexpected files to delete").green
257
+ end
258
+ end
259
+
260
+ def clean_generated_directories_with_feedback
261
+ directories_to_clean = [
262
+ generated_packs_directory_path,
263
+ generated_server_bundle_directory_path
264
+ ].compact.uniq
265
+
266
+ puts Rainbow("🧹 Cleaning generated directories...").yellow
267
+
268
+ total_deleted = directories_to_clean.sum { |dir_path| clean_directory_with_feedback(dir_path) }
269
+
270
+ if total_deleted.positive?
271
+ puts Rainbow("🗑️ Deleted #{total_deleted} generated files total").red
272
+ else
273
+ puts Rainbow("✨ No files to delete, directories are clean").green
274
+ end
275
+ end
276
+
277
+ def clean_directory_with_feedback(dir_path)
278
+ return create_directory_with_feedback(dir_path) unless Dir.exist?(dir_path)
279
+
280
+ files = Dir.glob("#{dir_path}/**/*").select { |f| File.file?(f) }
281
+
282
+ if files.any?
283
+ puts Rainbow(" Deleting #{files.length} files from #{dir_path}:").cyan
284
+ files.each { |file| puts Rainbow(" - #{File.basename(file)}").blue }
285
+ FileUtils.rm_rf(dir_path)
286
+ FileUtils.mkdir_p(dir_path)
287
+ files.length
288
+ else
289
+ puts Rainbow(" Directory #{dir_path} is already empty").cyan
290
+ FileUtils.rm_rf(dir_path)
291
+ FileUtils.mkdir_p(dir_path)
292
+ 0
293
+ end
294
+ end
295
+
296
+ def create_directory_with_feedback(dir_path)
297
+ puts Rainbow(" Directory #{dir_path} does not exist, creating...").cyan
298
+ FileUtils.mkdir_p(dir_path)
299
+ 0
188
300
  end
189
301
 
190
302
  def server_bundle_entrypoint
@@ -198,6 +310,13 @@ module ReactOnRails
198
310
  "#{source_entry_path}/generated"
199
311
  end
200
312
 
313
+ def generated_server_bundle_directory_path
314
+ return nil if ReactOnRails.configuration.make_generated_server_bundle_the_entrypoint
315
+
316
+ source_entrypoint_parent = Pathname(ReactOnRails::PackerUtils.packer_source_entry_path).parent
317
+ "#{source_entrypoint_parent}/generated"
318
+ end
319
+
201
320
  def relative_component_path_from_generated_pack(ror_component_path)
202
321
  component_file_pathname = Pathname.new(ror_component_path)
203
322
  component_generated_pack_path = generated_pack_path(ror_component_path)
@@ -228,14 +347,20 @@ module ReactOnRails
228
347
  paths.to_h { |path| [component_name(path), path] }
229
348
  end
230
349
 
350
+ def filter_component_files(paths)
351
+ paths.grep(COMPONENT_EXTENSIONS)
352
+ end
353
+
231
354
  def common_component_to_path
232
355
  common_components_paths = Dir.glob("#{components_search_path}/*").grep_v(CONTAINS_CLIENT_OR_SERVER_REGEX)
233
- component_name_to_path(common_components_paths)
356
+ filtered_paths = filter_component_files(common_components_paths)
357
+ component_name_to_path(filtered_paths)
234
358
  end
235
359
 
236
360
  def client_component_to_path
237
361
  client_render_components_paths = Dir.glob("#{components_search_path}/*.client.*")
238
- client_specific_components = component_name_to_path(client_render_components_paths)
362
+ filtered_client_paths = filter_component_files(client_render_components_paths)
363
+ client_specific_components = component_name_to_path(filtered_client_paths)
239
364
 
240
365
  duplicate_components = common_component_to_path.slice(*client_specific_components.keys)
241
366
  duplicate_components.each_key { |component| raise_client_component_overrides_common(component) }
@@ -245,7 +370,8 @@ module ReactOnRails
245
370
 
246
371
  def server_component_to_path
247
372
  server_render_components_paths = Dir.glob("#{components_search_path}/*.server.*")
248
- server_specific_components = component_name_to_path(server_render_components_paths)
373
+ filtered_server_paths = filter_component_files(server_render_components_paths)
374
+ server_specific_components = component_name_to_path(filtered_server_paths)
249
375
 
250
376
  duplicate_components = common_component_to_path.slice(*server_specific_components.keys)
251
377
  duplicate_components.each_key { |component| raise_server_component_overrides_common(component) }
@@ -0,0 +1,21 @@
1
+ # React on Rails Pro License
2
+
3
+ The files in this directory and its subdirectories are licensed under the **React on Rails Pro** license, which is separate from the MIT license that covers the core React on Rails functionality.
4
+
5
+ ## License Terms
6
+
7
+ These files are proprietary software and are **NOT** covered by the MIT license found in the root LICENSE.md file. Usage requires a valid React on Rails Pro license.
8
+
9
+ ## Distribution
10
+
11
+ Files in this directory will be **omitted** from future distributions of the open source React on Rails Ruby gem. They are exclusively available to React on Rails Pro licensees.
12
+
13
+ ## License Reference
14
+
15
+ For the complete React on Rails Pro license terms, see: `REACT-ON-RAILS-PRO-LICENSE.md` in the root directory of this repository.
16
+
17
+ ## More Information
18
+
19
+ For React on Rails Pro licensing information and to obtain a license, please visit:
20
+ - [React on Rails Pro](https://www.shakacode.com/react-on-rails-pro/)
21
+ - Contact: [react_on_rails@shakacode.com](mailto:react_on_rails@shakacode.com)
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ # /*
4
+ # * Copyright (c) 2025 Shakacode LLC
5
+ # *
6
+ # * This file is NOT licensed under the MIT (open source) license.
7
+ # * It is part of the React on Rails Pro offering and is licensed separately.
8
+ # *
9
+ # * Unauthorized copying, modification, distribution, or use of this file,
10
+ # * via any medium, is strictly prohibited without a valid license agreement
11
+ # * from Shakacode LLC.
12
+ # *
13
+ # * For licensing terms, please see:
14
+ # * https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
15
+ # */
16
+
17
+ module ReactOnRails
18
+ module Pro
19
+ module Helper
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."
23
+
24
+ # Generates the complete component specification script tag.
25
+ # Handles both immediate hydration (Pro feature) and standard cases.
26
+ def generate_component_script(render_options)
27
+ # Setup the page_loaded_js, which is the same regardless of prerendering or not!
28
+ # The reason is that React is smart about not doing extra work if the server rendering did its job.
29
+ component_specification_tag = content_tag(:script,
30
+ json_safe_and_pretty(render_options.client_props).html_safe,
31
+ type: "application/json",
32
+ class: "js-react-on-rails-component",
33
+ id: "js-react-on-rails-component-#{render_options.dom_id}",
34
+ "data-component-name" => render_options.react_component_name,
35
+ "data-trace" => (render_options.trace ? true : nil),
36
+ "data-dom-id" => render_options.dom_id,
37
+ "data-store-dependencies" =>
38
+ render_options.store_dependencies&.to_json,
39
+ "data-immediate-hydration" =>
40
+ (render_options.immediate_hydration ? true : nil))
41
+
42
+ # Add immediate invocation script if immediate hydration is enabled
43
+ spec_tag = if render_options.immediate_hydration
44
+ # Escape dom_id for JavaScript context
45
+ escaped_dom_id = escape_javascript(render_options.dom_id)
46
+ immediate_script = content_tag(:script, %(
47
+ typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsComponentLoaded('#{escaped_dom_id}');
48
+ ).html_safe)
49
+ "#{component_specification_tag}\n#{immediate_script}"
50
+ else
51
+ component_specification_tag
52
+ end
53
+
54
+ pro_warning_badge = pro_warning_badge_if_needed(render_options.explicitly_disabled_pro_options)
55
+ "#{pro_warning_badge}\n#{spec_tag}".html_safe
56
+ end
57
+
58
+ # Generates the complete store hydration script tag.
59
+ # Handles both immediate hydration (Pro feature) and standard cases.
60
+ def generate_store_script(redux_store_data)
61
+ pro_options_check_result = ReactOnRails::Pro::Utils.disable_pro_render_options_if_not_licensed(redux_store_data)
62
+ redux_store_data = pro_options_check_result[:raw_options]
63
+ explicitly_disabled_pro_options = pro_options_check_result[:explicitly_disabled_pro_options]
64
+
65
+ store_hydration_data = content_tag(:script,
66
+ json_safe_and_pretty(redux_store_data[:props]).html_safe,
67
+ type: "application/json",
68
+ "data-js-react-on-rails-store" => redux_store_data[:store_name].html_safe,
69
+ "data-immediate-hydration" =>
70
+ (redux_store_data[:immediate_hydration] ? true : nil))
71
+
72
+ # Add immediate invocation script if immediate hydration is enabled and Pro license is valid
73
+ store_hydration_scripts = if redux_store_data[:immediate_hydration]
74
+ # Escape store_name for JavaScript context
75
+ escaped_store_name = escape_javascript(redux_store_data[:store_name])
76
+ immediate_script = content_tag(:script, <<~JS.strip_heredoc.html_safe
77
+ typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsStoreLoaded('#{escaped_store_name}');
78
+ JS
79
+ )
80
+ "#{store_hydration_data}\n#{immediate_script}"
81
+ else
82
+ store_hydration_data
83
+ end
84
+
85
+ pro_warning_badge = pro_warning_badge_if_needed(explicitly_disabled_pro_options)
86
+ "#{pro_warning_badge}\n#{store_hydration_scripts}".html_safe
87
+ end
88
+
89
+ def pro_warning_badge_if_needed(explicitly_disabled_pro_options)
90
+ return "" unless explicitly_disabled_pro_options.any?
91
+
92
+ disabled_features_message = disabled_pro_features_message(explicitly_disabled_pro_options)
93
+ warning_message = "[REACT ON RAILS] #{disabled_features_message}\n" \
94
+ "Please visit https://shakacode.com/react-on-rails-pro to learn more."
95
+ puts warning_message
96
+ Rails.logger.warn warning_message
97
+
98
+ tooltip_text = "#{disabled_features_message} Click to learn more."
99
+
100
+ <<~HTML.strip
101
+ <a href="https://shakacode.com/react-on-rails-pro" target="_blank" rel="noopener noreferrer" title="#{tooltip_text}">
102
+ <div style="position: fixed; top: 0; right: 0; width: 180px; height: 180px; overflow: hidden; z-index: 9999; pointer-events: none;">
103
+ <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;">
104
+ React On Rails Pro Required
105
+ </div>
106
+ </div>
107
+ </a>
108
+ HTML
109
+ end
110
+
111
+ def disabled_pro_features_message(explicitly_disabled_pro_options)
112
+ return "".html_safe unless explicitly_disabled_pro_options.any?
113
+
114
+ feature_list = explicitly_disabled_pro_options.join(", ")
115
+ feature_word = explicitly_disabled_pro_options.size == 1 ? "feature" : "features"
116
+ "The '#{feature_list}' #{feature_word} " \
117
+ "#{explicitly_disabled_pro_options.size == 1 ? 'requires' : 'require'} a " \
118
+ "React on Rails Pro license. "
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ # /*
4
+ # * Copyright (c) 2025 Shakacode LLC
5
+ # *
6
+ # * This file is NOT licensed under the MIT (open source) license.
7
+ # * It is part of the React on Rails Pro offering and is licensed separately.
8
+ # *
9
+ # * Unauthorized copying, modification, distribution, or use of this file,
10
+ # * via any medium, is strictly prohibited without a valid license agreement
11
+ # * from Shakacode LLC.
12
+ # *
13
+ # * For licensing terms, please see:
14
+ # * https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
15
+ # */
16
+
17
+ module ReactOnRails
18
+ module Pro
19
+ module Utils
20
+ PRO_ONLY_OPTIONS = %i[immediate_hydration].freeze
21
+
22
+ # Checks if React on Rails Pro features are available
23
+ # @return [Boolean] true if Pro license is valid, false otherwise
24
+ def self.support_pro_features?
25
+ ReactOnRails::Utils.react_on_rails_pro_licence_valid?
26
+ end
27
+
28
+ def self.disable_pro_render_options_if_not_licensed(raw_options)
29
+ if support_pro_features?
30
+ return {
31
+ raw_options: raw_options,
32
+ explicitly_disabled_pro_options: []
33
+ }
34
+ end
35
+
36
+ raw_options_after_disable = raw_options.dup
37
+
38
+ explicitly_disabled_pro_options = PRO_ONLY_OPTIONS.select do |option|
39
+ # Use global configuration if it's not overridden in the options
40
+ next ReactOnRails.configuration.send(option) if raw_options[option].nil?
41
+
42
+ raw_options[option]
43
+ end
44
+ explicitly_disabled_pro_options.each { |option| raw_options_after_disable[option] = false }
45
+
46
+ {
47
+ raw_options: raw_options_after_disable,
48
+ explicitly_disabled_pro_options: explicitly_disabled_pro_options
49
+ }
50
+ end
51
+ end
52
+ end
53
+ end