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
@@ -7,16 +7,17 @@
7
7
  # 1. The white spacing in this file matters!
8
8
  # 2. Keep all #{some_var} fully to the left so that all indentation is done evenly in that var
9
9
  require "react_on_rails/prerender_error"
10
+ require "react_on_rails/smart_error"
10
11
  require "addressable/uri"
11
12
  require "react_on_rails/utils"
12
13
  require "react_on_rails/json_output"
13
14
  require "active_support/concern"
14
- require "react_on_rails/pro/helper"
15
+ require "react_on_rails/pro_helper"
15
16
 
16
17
  module ReactOnRails
17
18
  module Helper
18
19
  include ReactOnRails::Utils::Required
19
- include ReactOnRails::Pro::Helper
20
+ include ReactOnRails::ProHelper
20
21
 
21
22
  COMPONENT_HTML_KEY = "componentHtml"
22
23
 
@@ -94,104 +95,6 @@ module ReactOnRails
94
95
  end
95
96
  end
96
97
 
97
- # Streams a server-side rendered React component using React's `renderToPipeableStream`.
98
- # Supports React 18 features like Suspense, concurrent rendering, and selective hydration.
99
- # Enables progressive rendering and improved performance for large components.
100
- #
101
- # Note: This function can only be used with React on Rails Pro.
102
- # The view that uses this function must be rendered using the
103
- # `stream_view_containing_react_components` method from the React on Rails Pro gem.
104
- #
105
- # Example of an async React component that can benefit from streaming:
106
- #
107
- # const AsyncComponent = async () => {
108
- # const data = await fetchData();
109
- # return <div>{data}</div>;
110
- # };
111
- #
112
- # function App() {
113
- # return (
114
- # <Suspense fallback={<div>Loading...</div>}>
115
- # <AsyncComponent />
116
- # </Suspense>
117
- # );
118
- # }
119
- #
120
- # @param [String] component_name Name of your registered component
121
- # @param [Hash] options Options for rendering
122
- # @option options [Hash] :props Props to pass to the react component
123
- # @option options [String] :dom_id DOM ID of the component container
124
- # @option options [Hash] :html_options Options passed to content_tag
125
- # @option options [Boolean] :trace Set to true to add extra debugging information to the HTML
126
- # @option options [Boolean] :raise_on_prerender_error Set to true to raise exceptions during server-side rendering
127
- # Any other options are passed to the content tag, including the id.
128
- def stream_react_component(component_name, options = {})
129
- # stream_react_component doesn't have the prerender option
130
- # Because setting prerender to false is equivalent to calling react_component with prerender: false
131
- options[:prerender] = true
132
- options = options.merge(immediate_hydration: true) unless options.key?(:immediate_hydration)
133
- run_stream_inside_fiber do
134
- internal_stream_react_component(component_name, options)
135
- end
136
- end
137
-
138
- # Renders the React Server Component (RSC) payload for a given component. This helper generates
139
- # a special format designed by React for serializing server components and transmitting them
140
- # to the client.
141
- #
142
- # @return [String] Returns a Newline Delimited JSON (NDJSON) stream where each line contains a JSON object with:
143
- # - html: The RSC payload containing the rendered server components and client component references
144
- # - consoleReplayScript: JavaScript to replay server-side console logs in the client
145
- # - hasErrors: Boolean indicating if any errors occurred during rendering
146
- # - isShellReady: Boolean indicating if the initial shell is ready for hydration
147
- #
148
- # Example NDJSON stream:
149
- # {"html":"<RSC Payload>","consoleReplayScript":"","hasErrors":false,"isShellReady":true}
150
- # {"html":"<RSC Payload>","consoleReplayScript":"console.log('Loading...')","hasErrors":false,"isShellReady":true}
151
- #
152
- # The RSC payload within the html field contains:
153
- # - The component's rendered output from the server
154
- # - References to client components that need hydration
155
- # - Data props passed to client components
156
- #
157
- # @param component_name [String] The name of the React component to render. This component should
158
- # be a server component or a mixed component tree containing both server and client components.
159
- #
160
- # @param options [Hash] Options for rendering the component
161
- # @option options [Hash] :props Props to pass to the component (default: {})
162
- # @option options [Boolean] :trace Enable tracing for debugging (default: false)
163
- # @option options [String] :id Custom DOM ID for the component container (optional)
164
- #
165
- # @example Basic usage with a server component
166
- # <%= rsc_payload_react_component("ReactServerComponentPage") %>
167
- #
168
- # @example With props and tracing enabled
169
- # <%= rsc_payload_react_component("RSCPostsPage",
170
- # props: { artificialDelay: 1000 },
171
- # trace: true) %>
172
- #
173
- # @note This helper requires React Server Components support to be enabled in your configuration:
174
- # ReactOnRailsPro.configure do |config|
175
- # config.enable_rsc_support = true
176
- # end
177
- #
178
- # @raise [ReactOnRailsPro::Error] if RSC support is not enabled in configuration
179
- #
180
- # @note You don't have to deal directly with this helper function - it's used internally by the
181
- # `rsc_payload_route` helper function. The returned data from this function is used internally by
182
- # components registered using the `registerServerComponent` function. Don't use it unless you need
183
- # more control over the RSC payload generation. To know more about RSC payload, see the following link:
184
- # @see https://www.shakacode.com/react-on-rails-pro/docs/how-react-server-components-works.md
185
- # for technical details about the RSC payload format
186
- def rsc_payload_react_component(component_name, options = {})
187
- # rsc_payload_react_component doesn't have the prerender option
188
- # Because setting prerender to false will not do anything
189
- options[:prerender] = true
190
- run_stream_inside_fiber do
191
- internal_rsc_payload_react_component(component_name, options)
192
- end
193
- end
194
-
195
98
  # react_component_hash is used to return multiple HTML strings for server rendering, such as for
196
99
  # adding meta-tags to a page.
197
100
  # It is exactly like react_component except for the following:
@@ -252,10 +155,10 @@ module ReactOnRails
252
155
  # props: Ruby Hash or JSON string which contains the properties to pass to the redux store.
253
156
  # Options
254
157
  # defer: false -- pass as true if you wish to render this below your component.
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.
158
+ # immediate_hydration: nil -- React on Rails Pro (licensed) feature. When nil (default), Pro users
159
+ # get immediate hydration, non-Pro users don't. Can be explicitly overridden.
257
160
  def redux_store(store_name, props: {}, defer: false, immediate_hydration: nil)
258
- immediate_hydration = ReactOnRails.configuration.immediate_hydration if immediate_hydration.nil?
161
+ immediate_hydration = ReactOnRails::Utils.normalize_immediate_hydration(immediate_hydration, store_name, "Store")
259
162
 
260
163
  redux_store_data = { store_name: store_name,
261
164
  props: props,
@@ -323,7 +226,7 @@ module ReactOnRails
323
226
  }
324
227
  }
325
228
 
326
- consoleReplayScript = ReactOnRails.buildConsoleReplay();
229
+ consoleReplayScript = ReactOnRails.getConsoleReplayScript();
327
230
 
328
231
  return JSON.stringify({
329
232
  html: htmlResult,
@@ -339,8 +242,9 @@ module ReactOnRails
339
242
  .server_render_js_with_console_logging(js_code, render_options)
340
243
 
341
244
  html = result["html"]
342
- console_log_script = result["consoleLogScript"]
343
- raw("#{html}#{console_log_script if render_options.replay_console}")
245
+ console_script = result["consoleReplayScript"]
246
+ console_script_tag = wrap_console_script_with_nonce(console_script) if render_options.replay_console
247
+ raw("#{html}#{console_script_tag}")
344
248
  rescue ExecJS::ProgramError => err
345
249
  raise ReactOnRails::PrerenderError.new(component_name: "N/A (server_render_js called)",
346
250
  err: err,
@@ -366,7 +270,7 @@ module ReactOnRails
366
270
  #
367
271
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
368
272
  def rails_context(server_side: true)
369
- # ALERT: Keep in sync with node_package/src/types/index.ts for the properties of RailsContext
273
+ # ALERT: Keep in sync with packages/react-on-rails/src/types/index.ts for the properties of RailsContext
370
274
  @rails_context ||= begin
371
275
  result = {
372
276
  componentRegistryTimeout: ReactOnRails.configuration.component_registry_timeout,
@@ -446,32 +350,6 @@ module ReactOnRails
446
350
 
447
351
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
448
352
 
449
- def run_stream_inside_fiber
450
- unless ReactOnRails::Utils.react_on_rails_pro?
451
- raise ReactOnRails::Error,
452
- "You must use React on Rails Pro to use the stream_react_component method."
453
- end
454
-
455
- if @rorp_rendering_fibers.nil?
456
- raise ReactOnRails::Error,
457
- "You must call stream_view_containing_react_components to render the view containing the react component"
458
- end
459
-
460
- rendering_fiber = Fiber.new do
461
- stream = yield
462
- stream.each_chunk do |chunk|
463
- Fiber.yield chunk
464
- end
465
- end
466
-
467
- @rorp_rendering_fibers << rendering_fiber
468
-
469
- # return the first chunk of the fiber
470
- # It contains the initial html of the component
471
- # all updates will be appended to the stream sent to browser
472
- rendering_fiber.resume
473
- end
474
-
475
353
  def registered_stores
476
354
  @registered_stores ||= []
477
355
  end
@@ -494,25 +372,6 @@ module ReactOnRails
494
372
  options: options)
495
373
  end
496
374
 
497
- def internal_stream_react_component(component_name, options = {})
498
- options = options.merge(render_mode: :html_streaming)
499
- result = internal_react_component(component_name, options)
500
- build_react_component_result_for_server_streamed_content(
501
- rendered_html_stream: result[:result],
502
- component_specification_tag: result[:tag],
503
- render_options: result[:render_options]
504
- )
505
- end
506
-
507
- def internal_rsc_payload_react_component(react_component_name, options = {})
508
- options = options.merge(render_mode: :rsc_payload_streaming)
509
- render_options = create_render_options(react_component_name, options)
510
- json_stream = server_rendered_react_component(render_options)
511
- json_stream.transform do |chunk|
512
- "#{chunk.to_json}\n".html_safe
513
- end
514
- end
515
-
516
375
  def generated_components_pack_path(component_name)
517
376
  "#{ReactOnRails::PackerUtils.packer_source_entry_path}/generated/#{component_name}.js"
518
377
  end
@@ -536,7 +395,7 @@ module ReactOnRails
536
395
  server_rendered_html.html_safe,
537
396
  content_tag_options)
538
397
 
539
- result_console_script = render_options.replay_console ? console_script : ""
398
+ result_console_script = render_options.replay_console ? wrap_console_script_with_nonce(console_script) : ""
540
399
  result = compose_react_component_html_with_spec_and_console(
541
400
  component_specification_tag, rendered_output, result_console_script
542
401
  )
@@ -544,32 +403,6 @@ module ReactOnRails
544
403
  prepend_render_rails_context(result)
545
404
  end
546
405
 
547
- def build_react_component_result_for_server_streamed_content(
548
- rendered_html_stream:,
549
- component_specification_tag:,
550
- render_options:
551
- )
552
- is_first_chunk = true
553
- rendered_html_stream.transform do |chunk_json_result|
554
- if is_first_chunk
555
- is_first_chunk = false
556
- build_react_component_result_for_server_rendered_string(
557
- server_rendered_html: chunk_json_result["html"],
558
- component_specification_tag: component_specification_tag,
559
- console_script: chunk_json_result["consoleReplayScript"],
560
- render_options: render_options
561
- )
562
- else
563
- result_console_script = render_options.replay_console ? chunk_json_result["consoleReplayScript"] : ""
564
- # No need to prepend component_specification_tag or add rails context again
565
- # as they're already included in the first chunk
566
- compose_react_component_html_with_spec_and_console(
567
- "", chunk_json_result["html"], result_console_script
568
- )
569
- end
570
- end
571
- end
572
-
573
406
  def build_react_component_result_for_server_rendered_hash(
574
407
  server_rendered_html: required("server_rendered_html"),
575
408
  component_specification_tag: required("component_specification_tag"),
@@ -587,7 +420,7 @@ module ReactOnRails
587
420
  server_rendered_html[COMPONENT_HTML_KEY].html_safe,
588
421
  content_tag_options)
589
422
 
590
- result_console_script = render_options.replay_console ? console_script : ""
423
+ result_console_script = render_options.replay_console ? wrap_console_script_with_nonce(console_script) : ""
591
424
  result = compose_react_component_html_with_spec_and_console(
592
425
  component_specification_tag, rendered_output, result_console_script
593
426
  )
@@ -604,6 +437,32 @@ module ReactOnRails
604
437
  )
605
438
  end
606
439
 
440
+ # Wraps console replay JavaScript code in a script tag with CSP nonce if available.
441
+ # The console_script_code is already sanitized by scriptSanitizedVal() in the JavaScript layer,
442
+ # so using html_safe here is secure.
443
+ def wrap_console_script_with_nonce(console_script_code)
444
+ return "" if console_script_code.blank?
445
+
446
+ # Get the CSP nonce if available (Rails 5.2+)
447
+ # Rails 5.2-6.0 use content_security_policy_nonce with no arguments
448
+ # Rails 6.1+ accept an optional directive argument
449
+ nonce = if respond_to?(:content_security_policy_nonce)
450
+ begin
451
+ content_security_policy_nonce(:script)
452
+ rescue ArgumentError
453
+ # Fallback for Rails versions that don't accept arguments
454
+ content_security_policy_nonce
455
+ end
456
+ end
457
+
458
+ # Build the script tag with nonce if available
459
+ script_options = { id: "consoleReplayLog" }
460
+ script_options[:nonce] = nonce if nonce.present?
461
+
462
+ # Safe to use html_safe because content is pre-sanitized via scriptSanitizedVal()
463
+ content_tag(:script, console_script_code.html_safe, script_options)
464
+ end
465
+
607
466
  def compose_react_component_html_with_spec_and_console(component_specification_tag, rendered_output,
608
467
  console_script)
609
468
  # IMPORTANT: Ensure that we mark string as html_safe to avoid escaping.
@@ -620,10 +479,23 @@ module ReactOnRails
620
479
 
621
480
  @rendered_rails_context = true
622
481
 
623
- content_tag(:script,
624
- json_safe_and_pretty(data).html_safe,
625
- type: "application/json",
626
- id: "js-react-on-rails-context")
482
+ attribution_comment = react_on_rails_attribution_comment
483
+ script_tag = content_tag(:script,
484
+ json_safe_and_pretty(data).html_safe,
485
+ type: "application/json",
486
+ id: "js-react-on-rails-context")
487
+
488
+ "#{attribution_comment}\n#{script_tag}".html_safe
489
+ end
490
+
491
+ # Generates the HTML attribution comment
492
+ # Pro version calls ReactOnRailsPro::Utils for license-specific details
493
+ def react_on_rails_attribution_comment
494
+ if ReactOnRails::Utils.react_on_rails_pro?
495
+ ReactOnRailsPro::Utils.pro_attribution_comment
496
+ else
497
+ "<!-- Powered by React on Rails (c) ShakaCode | Open Source -->"
498
+ end
627
499
  end
628
500
 
629
501
  # prepend the rails_context if not yet applied
@@ -742,6 +614,15 @@ module ReactOnRails
742
614
  # It doesn't make any transformation, it listens and raises error if a chunk has errors
743
615
  chunk_json_result
744
616
  end
617
+
618
+ result.rescue do |err|
619
+ # This error came from the renderer
620
+ raise ReactOnRails::PrerenderError.new(component_name: react_component_name,
621
+ # Sanitize as this might be browser logged
622
+ props: sanitized_props_string(props),
623
+ err: err,
624
+ js_code: js_code)
625
+ end
745
626
  elsif result["hasErrors"] && render_options.raise_on_prerender_error
746
627
  raise_prerender_error(result, react_component_name, props, js_code)
747
628
  end
@@ -794,14 +675,11 @@ module ReactOnRails
794
675
  end
795
676
 
796
677
  def raise_missing_autoloaded_bundle(react_component_name)
797
- msg = <<~MSG
798
- **ERROR** ReactOnRails: Component "#{react_component_name}" is configured as "auto_load_bundle: true"
799
- but the generated component entrypoint, which should have been at #{generated_components_pack_path(react_component_name)},
800
- is missing. You might want to check that this component is in a directory named "#{ReactOnRails.configuration.components_subdirectory}"
801
- & that "bundle exec rake react_on_rails:generate_packs" has been run.
802
- MSG
803
-
804
- raise ReactOnRails::Error, msg
678
+ raise ReactOnRails::SmartError.new(
679
+ error_type: :missing_auto_loaded_bundle,
680
+ component_name: react_component_name,
681
+ expected_path: generated_components_pack_path(react_component_name)
682
+ )
805
683
  end
806
684
  end
807
685
  end
@@ -4,7 +4,7 @@ require "erb"
4
4
 
5
5
  module ReactOnRails
6
6
  module Locales
7
- def self.compile
7
+ def self.compile(force: false)
8
8
  config = ReactOnRails.configuration
9
9
  check_config_directory_exists(
10
10
  directory: config.i18n_dir, key_name: "config.i18n_dir",
@@ -15,9 +15,9 @@ module ReactOnRails
15
15
  remove_if: "not using this i18n with React on Rails, or if you want to use all translation files"
16
16
  )
17
17
  if config.i18n_output_format&.downcase == "js"
18
- ReactOnRails::Locales::ToJs.new
18
+ ReactOnRails::Locales::ToJs.new(force: force)
19
19
  else
20
- ReactOnRails::Locales::ToJson.new
20
+ ReactOnRails::Locales::ToJson.new(force: force)
21
21
  end
22
22
  end
23
23
 
@@ -36,12 +36,23 @@ module ReactOnRails
36
36
  private_class_method :check_config_directory_exists
37
37
 
38
38
  class Base
39
- def initialize
39
+ def initialize(force: false)
40
40
  return if i18n_dir.nil?
41
- return unless obsolete?
41
+
42
+ if locale_files.empty?
43
+ puts "Warning: No locale files found in #{i18n_yml_dir || 'Rails i18n load path'}"
44
+ return
45
+ end
46
+
47
+ if !force && !obsolete?
48
+ puts "Locale files are up to date, skipping generation. " \
49
+ "Use 'rake react_on_rails:locale force=true' to force regeneration."
50
+ return
51
+ end
42
52
 
43
53
  @translations, @defaults = generate_translations
44
54
  convert
55
+ puts "Generated locale files in #{i18n_dir}"
45
56
  end
46
57
 
47
58
  private
@@ -49,6 +60,7 @@ module ReactOnRails
49
60
  def file_format; end
50
61
 
51
62
  def obsolete?
63
+ return true if exist_files.length != files.length # Some files missing
52
64
  return true if exist_files.empty?
53
65
 
54
66
  files_are_outdated
@@ -55,8 +55,12 @@ module ReactOnRails
55
55
  # the webpack-dev-server is provided by the config value
56
56
  # "same_bundle_for_client_and_server" where a value of true
57
57
  # would mean that the bundle is created by the webpack-dev-server
58
- is_bundle_running_on_server = (bundle_name == ReactOnRails.configuration.server_bundle_js_file) ||
59
- (bundle_name == ReactOnRails.configuration.rsc_bundle_js_file)
58
+ is_bundle_running_on_server = bundle_name == ReactOnRails.configuration.server_bundle_js_file
59
+
60
+ # Check Pro RSC bundle if Pro is available
61
+ if ReactOnRails::Utils.react_on_rails_pro?
62
+ is_bundle_running_on_server ||= (bundle_name == ReactOnRailsPro.configuration.rsc_bundle_js_file)
63
+ end
60
64
 
61
65
  if ::Shakapacker.dev_server.running? && (!is_bundle_running_on_server ||
62
66
  ReactOnRails.configuration.same_bundle_for_client_and_server)
@@ -162,5 +166,78 @@ module ReactOnRails
162
166
 
163
167
  raise ReactOnRails::Error, msg
164
168
  end
169
+
170
+ # Check if shakapacker.yml has a precompile hook configured
171
+ # This prevents react_on_rails from running generate_packs twice
172
+ #
173
+ # Returns false if detection fails for any reason (missing shakapacker, malformed config, etc.)
174
+ # to ensure generate_packs runs rather than being incorrectly skipped
175
+ #
176
+ # Note: Currently checks a single hook value. Future enhancement will support hook lists
177
+ # to allow prepending/appending multiple commands. See related Shakapacker issue for details.
178
+ def self.shakapacker_precompile_hook_configured?
179
+ return false unless defined?(::Shakapacker)
180
+
181
+ hook_value = extract_precompile_hook
182
+ return false if hook_value.nil?
183
+
184
+ hook_contains_generate_packs?(hook_value)
185
+ rescue StandardError => e
186
+ # Swallow errors during hook detection to fail safe - if we can't detect the hook,
187
+ # we should run generate_packs rather than skip it incorrectly.
188
+ # Possible errors: NoMethodError (config method missing), TypeError (unexpected data structure),
189
+ # or errors from shakapacker's internal implementation changes
190
+ warn "Warning: Unable to detect shakapacker precompile hook: #{e.message}" if ENV["DEBUG"]
191
+ false
192
+ end
193
+
194
+ def self.extract_precompile_hook
195
+ # Access config data using private :data method since there's no public API
196
+ # to access the raw configuration hash needed for hook detection
197
+ config_data = ::Shakapacker.config.send(:data)
198
+
199
+ # Try symbol keys first (Shakapacker's internal format), then fall back to string keys
200
+ # The key is 'precompile_hook' at the top level of the config
201
+ config_data&.[](:precompile_hook) || config_data&.[]("precompile_hook")
202
+ end
203
+
204
+ def self.hook_contains_generate_packs?(hook_value)
205
+ # The hook value can be either:
206
+ # 1. A direct command containing the rake task
207
+ # 2. A path to a script file that needs to be read
208
+ return false if hook_value.blank?
209
+
210
+ # Check if it's a direct command first
211
+ return true if hook_value.to_s.match?(/\breact_on_rails:generate_packs\b/)
212
+
213
+ # Check if it's a script file path
214
+ script_path = resolve_hook_script_path(hook_value)
215
+ return false unless script_path && File.exist?(script_path)
216
+
217
+ # Read and check script contents
218
+ script_contents = File.read(script_path)
219
+ script_contents.match?(/\breact_on_rails:generate_packs\b/)
220
+ rescue StandardError
221
+ # If we can't read the script, assume it doesn't contain generate_packs
222
+ false
223
+ end
224
+
225
+ def self.resolve_hook_script_path(hook_value)
226
+ # Hook value might be a script path relative to Rails root
227
+ return nil unless defined?(Rails) && Rails.respond_to?(:root)
228
+
229
+ potential_path = Rails.root.join(hook_value.to_s.strip)
230
+ potential_path if potential_path.file?
231
+ end
232
+
233
+ # Returns the configured precompile hook value for logging/debugging
234
+ # Returns nil if no hook is configured
235
+ def self.shakapacker_precompile_hook_value
236
+ return nil unless defined?(::Shakapacker)
237
+
238
+ extract_precompile_hook
239
+ rescue StandardError
240
+ nil
241
+ end
165
242
  end
166
243
  end