react_on_rails 14.2.1 → 16.1.1

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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/AI_AGENT_INSTRUCTIONS.md +63 -0
  3. data/CHANGELOG.md +564 -85
  4. data/CLAUDE.md +135 -0
  5. data/CODING_AGENTS.md +313 -0
  6. data/CONTRIBUTING.md +448 -37
  7. data/Gemfile.development_dependencies +6 -1
  8. data/Gemfile.lock +13 -4
  9. data/KUDOS.md +22 -1
  10. data/LICENSE.md +30 -4
  11. data/LICENSES/README.md +14 -0
  12. data/NEWS.md +48 -48
  13. data/PROJECTS.md +45 -40
  14. data/REACT-ON-RAILS-PRO-LICENSE.md +129 -0
  15. data/README.md +113 -42
  16. data/SUMMARY.md +62 -52
  17. data/TODO.md +135 -0
  18. data/bin/lefthook/check-trailing-newlines +38 -0
  19. data/bin/lefthook/get-changed-files +26 -0
  20. data/bin/lefthook/prettier-format +26 -0
  21. data/bin/lefthook/ruby-autofix +26 -0
  22. data/bin/lefthook/ruby-lint +27 -0
  23. data/eslint.config.ts +232 -0
  24. data/knip.ts +40 -6
  25. data/lib/generators/USAGE +4 -5
  26. data/lib/generators/react_on_rails/USAGE +65 -0
  27. data/lib/generators/react_on_rails/base_generator.rb +276 -62
  28. data/lib/generators/react_on_rails/dev_tests_generator.rb +1 -0
  29. data/lib/generators/react_on_rails/generator_helper.rb +35 -1
  30. data/lib/generators/react_on_rails/generator_messages.rb +138 -17
  31. data/lib/generators/react_on_rails/install_generator.rb +474 -26
  32. data/lib/generators/react_on_rails/react_no_redux_generator.rb +19 -6
  33. data/lib/generators/react_on_rails/react_with_redux_generator.rb +110 -18
  34. data/lib/generators/react_on_rails/templates/.eslintrc +1 -1
  35. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev +5 -0
  36. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev-prod-assets +8 -0
  37. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev-static-assets +2 -0
  38. data/lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx +0 -5
  39. data/lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorld.module.css +2 -2
  40. data/lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorldServer.js +1 -1
  41. data/lib/generators/react_on_rails/templates/base/base/app/javascript/packs/server-bundle.js +1 -8
  42. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.jsx +21 -0
  43. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.tsx +25 -0
  44. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css +4 -0
  45. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.server.jsx +5 -0
  46. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.server.tsx +5 -0
  47. data/lib/generators/react_on_rails/templates/base/base/app/views/hello_world/index.html.erb.tt +1 -1
  48. data/lib/generators/react_on_rails/templates/base/base/app/views/layouts/hello_world.html.erb +4 -2
  49. data/lib/generators/react_on_rails/templates/base/base/babel.config.js.tt +5 -2
  50. data/lib/generators/react_on_rails/templates/base/base/bin/dev +34 -0
  51. data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +14 -5
  52. data/lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml +76 -7
  53. data/lib/generators/react_on_rails/templates/base/base/config/webpack/commonWebpackConfig.js.tt +1 -1
  54. data/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt +6 -10
  55. data/lib/generators/react_on_rails/templates/base/base/config/webpack/production.js.tt +2 -2
  56. data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +3 -2
  57. data/lib/generators/react_on_rails/templates/base/base/config/webpack/test.js.tt +2 -2
  58. data/lib/generators/react_on_rails/templates/dev_tests/spec/system/hello_world_spec.rb +0 -2
  59. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/actions/helloWorldActionCreators.ts +18 -0
  60. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx +0 -6
  61. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.module.css +4 -0
  62. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.tsx +24 -0
  63. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/constants/helloWorldConstants.ts +6 -0
  64. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/containers/HelloWorldContainer.ts +20 -0
  65. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/reducers/helloWorldReducer.js +1 -1
  66. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/reducers/helloWorldReducer.ts +22 -0
  67. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.client.tsx +23 -0
  68. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.jsx +5 -0
  69. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.tsx +5 -0
  70. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/store/helloWorldStore.ts +18 -0
  71. data/lib/react_on_rails/configuration.rb +141 -57
  72. data/lib/react_on_rails/controller.rb +6 -2
  73. data/lib/react_on_rails/dev/file_manager.rb +78 -0
  74. data/lib/react_on_rails/dev/pack_generator.rb +27 -0
  75. data/lib/react_on_rails/dev/process_manager.rb +61 -0
  76. data/lib/react_on_rails/dev/server_manager.rb +487 -0
  77. data/lib/react_on_rails/dev.rb +20 -0
  78. data/lib/react_on_rails/doctor.rb +1149 -0
  79. data/lib/react_on_rails/engine.rb +6 -0
  80. data/lib/react_on_rails/git_utils.rb +12 -2
  81. data/lib/react_on_rails/helper.rb +176 -74
  82. data/lib/react_on_rails/json_parse_error.rb +6 -1
  83. data/lib/react_on_rails/packer_utils.rb +61 -71
  84. data/lib/react_on_rails/packs_generator.rb +221 -19
  85. data/lib/react_on_rails/prerender_error.rb +4 -0
  86. data/lib/react_on_rails/pro/NOTICE +21 -0
  87. data/lib/react_on_rails/pro/helper.rb +122 -0
  88. data/lib/react_on_rails/pro/utils.rb +53 -0
  89. data/lib/react_on_rails/react_component/render_options.rb +38 -6
  90. data/lib/react_on_rails/server_rendering_js_code.rb +0 -1
  91. data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +12 -5
  92. data/lib/react_on_rails/system_checker.rb +659 -0
  93. data/lib/react_on_rails/test_helper/webpack_assets_compiler.rb +1 -1
  94. data/lib/react_on_rails/test_helper/webpack_assets_status_checker.rb +6 -4
  95. data/lib/react_on_rails/test_helper.rb +2 -3
  96. data/lib/react_on_rails/utils.rb +139 -43
  97. data/lib/react_on_rails/version.rb +1 -1
  98. data/lib/react_on_rails/version_checker.rb +14 -20
  99. data/lib/react_on_rails/version_syntax_converter.rb +1 -1
  100. data/lib/react_on_rails.rb +1 -0
  101. data/lib/tasks/assets.rake +1 -1
  102. data/lib/tasks/doctor.rake +48 -0
  103. data/lib/tasks/generate_packs.rake +158 -1
  104. data/react_on_rails.gemspec +1 -0
  105. data/tsconfig.eslint.json +6 -0
  106. data/tsconfig.json +5 -3
  107. metadata +63 -14
  108. data/REACT-ON-RAILS-PRO-LICENSE +0 -95
  109. data/lib/generators/react_on_rails/adapt_for_older_shakapacker_generator.rb +0 -41
  110. data/lib/generators/react_on_rails/bin/dev +0 -30
  111. data/lib/generators/react_on_rails/bin/dev-static +0 -30
  112. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev-static.tt +0 -9
  113. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev.tt +0 -5
  114. data/lib/generators/react_on_rails/templates/base/base/app/javascript/packs/registration.js.tt +0 -8
  115. /data/lib/generators/react_on_rails/templates/base/base/config/webpack/{webpackConfig.js.tt → generateWebpackConfigs.js.tt} +0 -0
  116. /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
 
@@ -32,7 +34,7 @@ module ReactOnRails
32
34
  #
33
35
  # Exposing the react_component_name is necessary to both a plain ReactComponent as well as
34
36
  # a generator:
35
- # See README.md for how to "register" your react components.
37
+ # See README.md for how to "register" your React components.
36
38
  # See spec/dummy/client/app/packs/server-bundle.js and
37
39
  # spec/dummy/client/app/packs/client-bundle.js for examples of this.
38
40
  #
@@ -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
@@ -119,34 +122,74 @@ module ReactOnRails
119
122
  # @option options [Hash] :props Props to pass to the react component
120
123
  # @option options [String] :dom_id DOM ID of the component container
121
124
  # @option options [Hash] :html_options Options passed to content_tag
122
- # @option options [Boolean] :prerender Set to false to disable server-side rendering
123
125
  # @option options [Boolean] :trace Set to true to add extra debugging information to the HTML
124
126
  # @option options [Boolean] :raise_on_prerender_error Set to true to raise exceptions during server-side rendering
125
127
  # Any other options are passed to the content tag, including the id.
126
128
  def stream_react_component(component_name, options = {})
127
- unless ReactOnRails::Utils.react_on_rails_pro?
128
- raise ReactOnRails::Error,
129
- "You must use React on Rails Pro to use the stream_react_component method."
130
- end
131
-
132
- if @rorp_rendering_fibers.nil?
133
- raise ReactOnRails::Error,
134
- "You must call stream_view_containing_react_components to render the view containing the react component"
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
135
  end
136
+ end
136
137
 
137
- rendering_fiber = Fiber.new do
138
- stream = internal_stream_react_component(component_name, options)
139
- stream.each_chunk do |chunk|
140
- Fiber.yield chunk
141
- end
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)
142
192
  end
143
-
144
- @rorp_rendering_fibers << rendering_fiber
145
-
146
- # return the first chunk of the fiber
147
- # It contains the initial html of the component
148
- # all updates will be appended to the stream sent to browser
149
- rendering_fiber.resume
150
193
  end
151
194
 
152
195
  # react_component_hash is used to return multiple HTML strings for server rendering, such as for
@@ -168,6 +211,7 @@ module ReactOnRails
168
211
  #
169
212
  def react_component_hash(component_name, options = {})
170
213
  options[:prerender] = true
214
+
171
215
  internal_result = internal_react_component(component_name, options)
172
216
  server_rendered_html = internal_result[:result]["html"]
173
217
  console_script = internal_result[:result]["consoleReplayScript"]
@@ -184,6 +228,7 @@ module ReactOnRails
184
228
  console_script: console_script,
185
229
  render_options: render_options
186
230
  )
231
+
187
232
  else
188
233
  msg = <<~MSG
189
234
  Render-Function used by react_component_hash for #{component_name} is expected to return
@@ -207,19 +252,22 @@ module ReactOnRails
207
252
  # props: Ruby Hash or JSON string which contains the properties to pass to the redux store.
208
253
  # Options
209
254
  # defer: false -- pass as true if you wish to render this below your component.
210
- def redux_store(store_name, props: {}, defer: false)
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
+
211
260
  redux_store_data = { store_name: store_name,
212
- props: props }
261
+ props: props,
262
+ immediate_hydration: immediate_hydration }
213
263
  if defer
214
- @registered_stores_defer_render ||= []
215
- @registered_stores_defer_render << redux_store_data
264
+ registered_stores_defer_render << redux_store_data
216
265
  "YOU SHOULD NOT SEE THIS ON YOUR VIEW -- Uses as a code block, like <% redux_store %> " \
217
266
  "and not <%= redux store %>"
218
267
  else
219
- @registered_stores ||= []
220
- @registered_stores << redux_store_data
268
+ registered_stores << redux_store_data
221
269
  result = render_redux_store_data(redux_store_data)
222
- prepend_render_rails_context(result)
270
+ prepend_render_rails_context(result).html_safe
223
271
  end
224
272
  end
225
273
 
@@ -229,9 +277,9 @@ module ReactOnRails
229
277
  # client side rendering of this hydration data, which is a hidden div with a matching class
230
278
  # that contains a data props.
231
279
  def redux_store_hydration_data
232
- return if @registered_stores_defer_render.blank?
280
+ return if registered_stores_defer_render.blank?
233
281
 
234
- @registered_stores_defer_render.reduce(+"") do |accum, redux_store_data|
282
+ registered_stores_defer_render.reduce(+"") do |accum, redux_store_data|
235
283
  accum << render_redux_store_data(redux_store_data)
236
284
  end.html_safe
237
285
  end
@@ -292,7 +340,7 @@ module ReactOnRails
292
340
 
293
341
  html = result["html"]
294
342
  console_log_script = result["consoleLogScript"]
295
- raw("#{html}#{render_options.replay_console ? console_log_script : ''}")
343
+ raw("#{html}#{console_log_script if render_options.replay_console}")
296
344
  rescue ExecJS::ProgramError => err
297
345
  raise ReactOnRails::PrerenderError.new(component_name: "N/A (server_render_js called)",
298
346
  err: err,
@@ -321,6 +369,7 @@ module ReactOnRails
321
369
  # ALERT: Keep in sync with node_package/src/types/index.ts for the properties of RailsContext
322
370
  @rails_context ||= begin
323
371
  result = {
372
+ componentRegistryTimeout: ReactOnRails.configuration.component_registry_timeout,
324
373
  railsEnv: Rails.env,
325
374
  inMailer: in_mailer?,
326
375
  # Locale settings
@@ -330,8 +379,14 @@ module ReactOnRails
330
379
  # TODO: v13 just use the version if existing
331
380
  rorPro: ReactOnRails::Utils.react_on_rails_pro?
332
381
  }
382
+
333
383
  if ReactOnRails::Utils.react_on_rails_pro?
334
384
  result[:rorProVersion] = ReactOnRails::Utils.react_on_rails_pro_version
385
+
386
+ if ReactOnRails::Utils.rsc_support_enabled?
387
+ rsc_payload_url = ReactOnRailsPro.configuration.rsc_payload_generation_url_path
388
+ result[:rscPayloadGenerationUrlPath] = rsc_payload_url
389
+ end
335
390
  end
336
391
 
337
392
  if defined?(request) && request.present?
@@ -352,7 +407,7 @@ module ReactOnRails
352
407
  result.merge!(
353
408
  # URL settings
354
409
  href: uri.to_s,
355
- location: "#{uri.path}#{uri.query.present? ? "?#{uri.query}" : ''}",
410
+ location: "#{uri.path}#{"?#{uri.query}" if uri.query.present?}",
356
411
  scheme: uri.scheme, # http
357
412
  host: uri.host, # foo.com
358
413
  port: uri.port,
@@ -379,17 +434,68 @@ module ReactOnRails
379
434
  is_component_pack_present = File.exist?(generated_components_pack_path(react_component_name))
380
435
  raise_missing_autoloaded_bundle(react_component_name) unless is_component_pack_present
381
436
  end
382
- append_javascript_pack_tag("generated/#{react_component_name}",
383
- defer: ReactOnRails.configuration.defer_generated_component_packs)
437
+
438
+ options = { defer: ReactOnRails.configuration.generated_component_packs_loading_strategy == :defer }
439
+ # Old versions of Shakapacker don't support async script tags.
440
+ # ReactOnRails.configure already validates if async loading is supported by the installed Shakapacker version.
441
+ # Therefore, we only need to pass the async option if the loading strategy is explicitly set to :async
442
+ options[:async] = true if ReactOnRails.configuration.generated_component_packs_loading_strategy == :async
443
+ append_javascript_pack_tag("generated/#{react_component_name}", **options)
384
444
  append_stylesheet_pack_tag("generated/#{react_component_name}")
385
445
  end
386
446
 
387
447
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
388
448
 
389
- private
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
+ def registered_stores
476
+ @registered_stores ||= []
477
+ end
478
+
479
+ def registered_stores_defer_render
480
+ @registered_stores_defer_render ||= []
481
+ end
482
+
483
+ def registered_stores_including_deferred
484
+ registered_stores + registered_stores_defer_render
485
+ end
486
+
487
+ def create_render_options(react_component_name, options)
488
+ # If no store dependencies are passed, default to all registered stores up till now
489
+ unless options.key?(:store_dependencies)
490
+ store_dependencies = registered_stores_including_deferred.map { |store| store[:store_name] }
491
+ options = options.merge(store_dependencies: store_dependencies.presence)
492
+ end
493
+ ReactOnRails::ReactComponent::RenderOptions.new(react_component_name: react_component_name,
494
+ options: options)
495
+ end
390
496
 
391
497
  def internal_stream_react_component(component_name, options = {})
392
- options = options.merge(stream?: true)
498
+ options = options.merge(render_mode: :html_streaming)
393
499
  result = internal_react_component(component_name, options)
394
500
  build_react_component_result_for_server_streamed_content(
395
501
  rendered_html_stream: result[:result],
@@ -398,6 +504,15 @@ module ReactOnRails
398
504
  )
399
505
  end
400
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
+
401
516
  def generated_components_pack_path(component_name)
402
517
  "#{ReactOnRails::PackerUtils.packer_source_entry_path}/generated/#{component_name}.js"
403
518
  end
@@ -489,14 +604,13 @@ module ReactOnRails
489
604
  )
490
605
  end
491
606
 
492
- def compose_react_component_html_with_spec_and_console(component_specification_tag, rendered_output, console_script)
607
+ def compose_react_component_html_with_spec_and_console(component_specification_tag, rendered_output,
608
+ console_script)
493
609
  # IMPORTANT: Ensure that we mark string as html_safe to avoid escaping.
494
- html_content = <<~HTML
495
- #{rendered_output}
496
- #{component_specification_tag}
497
- #{console_script}
498
- HTML
499
- html_content.strip.html_safe
610
+ added_html = "#{component_specification_tag}\n#{console_script}".strip
611
+ added_html = added_html.present? ? "\n#{added_html}" : ""
612
+
613
+ "#{rendered_output}#{added_html}".html_safe
500
614
  end
501
615
 
502
616
  def rails_context_if_not_already_rendered
@@ -514,7 +628,9 @@ module ReactOnRails
514
628
 
515
629
  # prepend the rails_context if not yet applied
516
630
  def prepend_render_rails_context(render_value)
517
- "#{rails_context_if_not_already_rendered}\n#{render_value}".strip.html_safe
631
+ rails_context_content = rails_context_if_not_already_rendered
632
+ rails_context_content = rails_context_content.present? ? "#{rails_context_content}\n" : ""
633
+ "#{rails_context_content}#{render_value}".html_safe
518
634
  end
519
635
 
520
636
  def internal_react_component(react_component_name, options = {})
@@ -525,26 +641,11 @@ module ReactOnRails
525
641
  # (re-hydrate the data). This enables react rendered on the client to see that the
526
642
  # server has already rendered the HTML.
527
643
 
528
- render_options = ReactOnRails::ReactComponent::RenderOptions.new(react_component_name: react_component_name,
529
- options: options)
644
+ render_options = create_render_options(react_component_name, options)
530
645
 
531
646
  # Setup the page_loaded_js, which is the same regardless of prerendering or not!
532
647
  # The reason is that React is smart about not doing extra work if the server rendering did its job.
533
- component_specification_tag = content_tag(:script,
534
- json_safe_and_pretty(render_options.client_props).html_safe,
535
- type: "application/json",
536
- class: "js-react-on-rails-component",
537
- "data-component-name" => render_options.react_component_name,
538
- "data-trace" => (render_options.trace ? true : nil),
539
- "data-dom-id" => render_options.dom_id)
540
-
541
- if render_options.force_load
542
- component_specification_tag.concat(
543
- content_tag(:script, %(
544
- ReactOnRails.reactOnRailsComponentLoaded('#{render_options.dom_id}');
545
- ).html_safe)
546
- )
547
- end
648
+ component_specification_tag = generate_component_script(render_options)
548
649
 
549
650
  load_pack_for_generated_component(react_component_name, render_options)
550
651
  # Create the HTML rendering part
@@ -558,12 +659,9 @@ ReactOnRails.reactOnRailsComponentLoaded('#{render_options.dom_id}');
558
659
  end
559
660
 
560
661
  def render_redux_store_data(redux_store_data)
561
- result = content_tag(:script,
562
- json_safe_and_pretty(redux_store_data[:props]).html_safe,
563
- type: "application/json",
564
- "data-js-react-on-rails-store" => redux_store_data[:store_name].html_safe)
662
+ store_hydration_data = generate_store_script(redux_store_data)
565
663
 
566
- prepend_render_rails_context(result)
664
+ prepend_render_rails_context(store_hydration_data)
567
665
  end
568
666
 
569
667
  def props_string(props)
@@ -620,7 +718,7 @@ ReactOnRails.reactOnRailsComponentLoaded('#{render_options.dom_id}');
620
718
  js_code = ReactOnRails::ServerRenderingJsCode.server_rendering_component_js_code(
621
719
  props_string: props_string(props).gsub("\u2028", '\u2028').gsub("\u2029", '\u2029'),
622
720
  rails_context: rails_context(server_side: true).to_json,
623
- redux_stores: initialize_redux_stores,
721
+ redux_stores: initialize_redux_stores(render_options),
624
722
  react_component_name: react_component_name,
625
723
  render_options: render_options
626
724
  )
@@ -636,7 +734,7 @@ ReactOnRails.reactOnRailsComponentLoaded('#{render_options.dom_id}');
636
734
  js_code: js_code)
637
735
  end
638
736
 
639
- if render_options.stream?
737
+ if render_options.streaming?
640
738
  result.transform do |chunk_json_result|
641
739
  if should_raise_streaming_prerender_error?(chunk_json_result, render_options)
642
740
  raise_prerender_error(chunk_json_result, react_component_name, props, js_code)
@@ -651,17 +749,20 @@ ReactOnRails.reactOnRailsComponentLoaded('#{render_options.dom_id}');
651
749
  result
652
750
  end
653
751
 
654
- def initialize_redux_stores
752
+ def initialize_redux_stores(render_options)
655
753
  result = +<<-JS
656
754
  ReactOnRails.clearHydratedStores();
657
755
  JS
658
756
 
659
- return result unless @registered_stores.present? || @registered_stores_defer_render.present?
757
+ store_dependencies = render_options.store_dependencies
758
+ return result unless store_dependencies.present?
660
759
 
661
760
  declarations = +"var reduxProps, store, storeGenerator;\n"
662
- all_stores = (@registered_stores || []) + (@registered_stores_defer_render || [])
761
+ store_objects = registered_stores_including_deferred.select do |store|
762
+ store_dependencies.include?(store[:store_name])
763
+ end
663
764
 
664
- result << all_stores.each_with_object(declarations) do |redux_store_data, memo|
765
+ result << store_objects.each_with_object(declarations) do |redux_store_data, memo|
665
766
  store_name = redux_store_data[:store_name]
666
767
  props = props_string(redux_store_data[:props])
667
768
  memo << <<-JS.strip_heredoc
@@ -687,6 +788,7 @@ ReactOnRails.reactOnRailsComponentLoaded('#{render_options.dom_id}');
687
788
 
688
789
  if defined?(ScoutApm)
689
790
  include ScoutApm::Tracer
791
+
690
792
  instrument_method :react_component, type: "ReactOnRails", name: "react_component"
691
793
  instrument_method :react_component_hash, type: "ReactOnRails", name: "react_component_hash"
692
794
  end
@@ -7,7 +7,12 @@ module ReactOnRails
7
7
  def initialize(parse_error:, json:)
8
8
  @json = json
9
9
  @original_error = parse_error
10
- super(parse_error.message)
10
+ message = <<~MSG
11
+ #{parse_error.message}
12
+
13
+ #{Utils.default_troubleshooting_section}
14
+ MSG
15
+ super(message)
11
16
  end
12
17
 
13
18
  def to_honeybadger_context