react_on_rails_pro 16.2.0.beta.8

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 (109) hide show
  1. checksums.yaml +7 -0
  2. data/.controlplane/Dockerfile +49 -0
  3. data/.controlplane/controlplane.yml +22 -0
  4. data/.controlplane/gvc.yml +25 -0
  5. data/.controlplane/postgres.yml +33 -0
  6. data/.controlplane/rails.yml +49 -0
  7. data/.controlplane/redis.yml +18 -0
  8. data/.gitignore +77 -0
  9. data/.prettierignore +12 -0
  10. data/.prettierrc +19 -0
  11. data/.rspec +2 -0
  12. data/.rubocop.yml +120 -0
  13. data/.scss-lint.yml +205 -0
  14. data/CHANGELOG.md +570 -0
  15. data/CI_SETUP.md +502 -0
  16. data/CONTRIBUTING.md +376 -0
  17. data/Dockerfile +63 -0
  18. data/Gemfile +8 -0
  19. data/Gemfile.development_dependencies +74 -0
  20. data/Gemfile.loader +32 -0
  21. data/Gemfile.lock +527 -0
  22. data/LICENSE +98 -0
  23. data/LICENSE_SETUP.md +272 -0
  24. data/README.md +577 -0
  25. data/Rakefile +13 -0
  26. data/app/controllers/react_on_rails_pro/rsc_payload_controller.rb +7 -0
  27. data/app/helpers/react_on_rails_pro_helper.rb +360 -0
  28. data/app/views/react_on_rails_pro/rsc_payload.html.erb +1 -0
  29. data/babel.config.js +4 -0
  30. data/docs/bundle-caching.md +205 -0
  31. data/docs/caching.md +234 -0
  32. data/docs/code-splitting-loadable-components.md +313 -0
  33. data/docs/code-splitting.md +349 -0
  34. data/docs/configuration.md +165 -0
  35. data/docs/contributors-info/onboarding-customers.md +6 -0
  36. data/docs/contributors-info/releasing.md +40 -0
  37. data/docs/contributors-info/style.md +33 -0
  38. data/docs/home-pro.md +146 -0
  39. data/docs/installation.md +203 -0
  40. data/docs/js-memory-leaks.md +22 -0
  41. data/docs/node-renderer/basics.md +92 -0
  42. data/docs/node-renderer/debugging.md +38 -0
  43. data/docs/node-renderer/error-reporting-and-tracing.md +160 -0
  44. data/docs/node-renderer/heroku.md +102 -0
  45. data/docs/node-renderer/js-configuration.md +91 -0
  46. data/docs/node-renderer/troubleshooting.md +5 -0
  47. data/docs/profiling-server-side-rendering-code.md +179 -0
  48. data/docs/react-server-components/add-streaming-and-interactivity.md +190 -0
  49. data/docs/react-server-components/create-without-ssr.md +448 -0
  50. data/docs/react-server-components/glossary.md +102 -0
  51. data/docs/react-server-components/how-react-server-components-work.md +243 -0
  52. data/docs/react-server-components/inside-client-components.md +332 -0
  53. data/docs/react-server-components/purpose-and-benefits.md +243 -0
  54. data/docs/react-server-components/rendering-flow.md +86 -0
  55. data/docs/react-server-components/selective-hydration-in-streamed-components.md +75 -0
  56. data/docs/react-server-components/server-side-rendering.md +72 -0
  57. data/docs/react-server-components/tutorial.md +19 -0
  58. data/docs/release-notes/4.0.md +94 -0
  59. data/docs/release-notes/v4-react-server-components.md +66 -0
  60. data/docs/ruby-api.md +11 -0
  61. data/docs/streaming-server-rendering.md +210 -0
  62. data/docs/troubleshooting.md +24 -0
  63. data/docs/updating.md +219 -0
  64. data/eslint.config.mjs +220 -0
  65. data/lib/react_on_rails_pro/assets_precompile.rb +230 -0
  66. data/lib/react_on_rails_pro/cache.rb +88 -0
  67. data/lib/react_on_rails_pro/concerns/rsc_payload_renderer.rb +38 -0
  68. data/lib/react_on_rails_pro/concerns/stream.rb +103 -0
  69. data/lib/react_on_rails_pro/configuration.rb +228 -0
  70. data/lib/react_on_rails_pro/constants.rb +8 -0
  71. data/lib/react_on_rails_pro/engine.rb +24 -0
  72. data/lib/react_on_rails_pro/error.rb +14 -0
  73. data/lib/react_on_rails_pro/license_public_key.rb +30 -0
  74. data/lib/react_on_rails_pro/license_validator.rb +188 -0
  75. data/lib/react_on_rails_pro/prepare_node_renderer_bundles.rb +40 -0
  76. data/lib/react_on_rails_pro/rendering_error.rb +5 -0
  77. data/lib/react_on_rails_pro/request.rb +318 -0
  78. data/lib/react_on_rails_pro/routes.rb +13 -0
  79. data/lib/react_on_rails_pro/server_rendering_js_code.rb +102 -0
  80. data/lib/react_on_rails_pro/server_rendering_pool/node_rendering_pool.rb +133 -0
  81. data/lib/react_on_rails_pro/server_rendering_pool/pro_rendering.rb +117 -0
  82. data/lib/react_on_rails_pro/stream_cache.rb +61 -0
  83. data/lib/react_on_rails_pro/stream_request.rb +170 -0
  84. data/lib/react_on_rails_pro/utils.rb +222 -0
  85. data/lib/react_on_rails_pro/v8_log_processor.rb +50 -0
  86. data/lib/react_on_rails_pro/version.rb +6 -0
  87. data/lib/react_on_rails_pro.rb +23 -0
  88. data/package-scripts.yml +109 -0
  89. data/package.json +159 -0
  90. data/rakelib/dummy_apps.rake +22 -0
  91. data/rakelib/lint.rake +32 -0
  92. data/rakelib/public_key_management.rake +155 -0
  93. data/rakelib/rbs.rake +47 -0
  94. data/rakelib/run_rspec.rake +81 -0
  95. data/rakelib/task_helpers.rb +45 -0
  96. data/rakelib/yard.rake +20 -0
  97. data/react_on_rails_pro.gemspec +47 -0
  98. data/readme-gen-docs.md +1 -0
  99. data/script/bootstrap +33 -0
  100. data/script/preinstall.js +31 -0
  101. data/script/setup +23 -0
  102. data/script/test +38 -0
  103. data/sig/react_on_rails_pro/cache.rbs +13 -0
  104. data/sig/react_on_rails_pro/configuration.rbs +100 -0
  105. data/sig/react_on_rails_pro/error.rbs +4 -0
  106. data/sig/react_on_rails_pro/utils.rbs +7 -0
  107. data/sig/react_on_rails_pro.rbs +5 -0
  108. data/yarn.lock +7599 -0
  109. metadata +319 -0
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReactOnRailsPro
4
+ class AssetsPrecompile # rubocop:disable Metrics/ClassLength
5
+ include Singleton
6
+
7
+ def remote_bundle_cache_adapter
8
+ unless ReactOnRailsPro.configuration.remote_bundle_cache_adapter.is_a?(Module)
9
+ raise ReactOnRailsPro::Error, "config.remote_bundle_cache_adapter must have a module assigned"
10
+ end
11
+
12
+ ReactOnRailsPro.configuration.remote_bundle_cache_adapter
13
+ end
14
+
15
+ def zipped_bundles_filename
16
+ "precompile-cache.#{bundles_cache_key}.production.gz"
17
+ end
18
+
19
+ def zipped_bundles_filepath
20
+ @zipped_bundles_filepath ||=
21
+ begin
22
+ FileUtils.mkdir_p(Rails.root.join("tmp", "bundle_cache"))
23
+ Rails.root.join("tmp", "bundle_cache", zipped_bundles_filename)
24
+ end
25
+ end
26
+
27
+ def bundles_cache_key
28
+ @bundles_cache_key ||=
29
+ begin
30
+ ReactOnRailsPro::Utils.rorp_puts "Calculating digest of bundle dependencies."
31
+ starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
32
+ cache_dependencies = [Shakapacker.config.source_path.join("**", "*")]
33
+ .union(ReactOnRailsPro.configuration.dependency_globs)
34
+ # Note, digest_of_globs removes excluded globs
35
+ digest = ReactOnRailsPro::Utils.digest_of_globs(cache_dependencies)
36
+ # Include the NODE_ENV in the digest
37
+ env_cache_keys = [
38
+ ReactOnRailsPro::VERSION,
39
+ ENV.fetch("RAILS_ENV", nil),
40
+ ENV.fetch("NODE_ENV", nil)
41
+ ]
42
+
43
+ if remote_bundle_cache_adapter.respond_to?(:cache_keys)
44
+ env_cache_keys += remote_bundle_cache_adapter.cache_keys
45
+ end
46
+ env_cache_keys.compact.each { |value| digest.update(value) }
47
+
48
+ result = digest.hexdigest
49
+ ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
50
+ elapsed = (ending - starting).round(2)
51
+ ReactOnRailsPro::Utils.rorp_puts "Completed calculating digest of bundle dependencies in #{elapsed} seconds."
52
+ result
53
+ end
54
+ end
55
+
56
+ def build_bundles
57
+ remote_bundle_cache_adapter.build
58
+ rescue RuntimeError
59
+ ReactOnRailsPro::Utils.rorp_puts "The custom config.remote_bundle_cache_adapter 'build' method raised an error:"
60
+ raise
61
+ end
62
+
63
+ def self.call
64
+ instance.build_or_fetch_bundles
65
+
66
+ ReactOnRailsPro::PrepareNodeRenderBundles.call if ReactOnRailsPro.configuration.node_renderer?
67
+ end
68
+
69
+ def build_or_fetch_bundles
70
+ if disable_precompile_cache?
71
+ build_bundles
72
+ return
73
+ end
74
+
75
+ begin
76
+ bundles_fetched = fetch_and_unzip_cached_bundles
77
+ rescue RuntimeError => e
78
+ ReactOnRailsPro::Utils.rorp_puts "An error occurred while attempting to fetch cached bundles."
79
+ ReactOnRailsPro::Utils.rorp_puts "This will be evaluated as a bundle cache miss."
80
+ ReactOnRailsPro::Utils.rorp_puts e.message
81
+ puts e.backtrace.join('\n')
82
+ bundles_fetched = false
83
+ end
84
+
85
+ return if bundles_fetched
86
+
87
+ build_bundles
88
+
89
+ begin
90
+ cache_bundles
91
+ rescue RuntimeError => e
92
+ ReactOnRailsPro::Utils.rorp_puts "An error occurred while attempting to cache the built bundles."
93
+ ReactOnRailsPro::Utils.rorp_puts e.message
94
+ puts e.backtrace.join('\n')
95
+ end
96
+ end
97
+
98
+ def disable_precompile_cache?
99
+ ENV["DISABLE_PRECOMPILE_CACHE"] == "true"
100
+ end
101
+
102
+ def fetch_bundles
103
+ ReactOnRailsPro::Utils.rorp_puts "Checking for a cached bundle: #{zipped_bundles_filename}"
104
+ begin
105
+ fetch_result = remote_bundle_cache_adapter.fetch(zipped_bundles_filename)
106
+ rescue RuntimeError
107
+ message = "An error was raised by the custom config.remote_bundle_cache_adapter 'fetch' " \
108
+ "method when called with { zipped_bundles_filename: #{zipped_bundles_filename} }"
109
+ ReactOnRailsPro::Utils.rorp_puts message
110
+ raise
111
+ end
112
+
113
+ if fetch_result
114
+ ReactOnRailsPro::Utils.rorp_puts "Remote bundle cache detected. Bundles will be restored to local cache."
115
+ File.binwrite(zipped_bundles_filepath, fetch_result)
116
+ true
117
+ else
118
+ ReactOnRailsPro::Utils.rorp_puts "Remote bundle cache not found."
119
+ false
120
+ end
121
+ end
122
+
123
+ def fetch_and_unzip_cached_bundles
124
+ if File.exist?(zipped_bundles_filepath)
125
+ ReactOnRailsPro::Utils.rorp_puts "Found a local cache of bundles: #{zipped_bundles_filepath}"
126
+ result = true
127
+ else
128
+ result = fetch_bundles
129
+ end
130
+
131
+ if File.exist?(zipped_bundles_filepath)
132
+ ReactOnRailsPro::Utils.rorp_puts "gunzipping bundle cache: #{zipped_bundles_filepath}"
133
+ public_output_path = Shakapacker.config.public_output_path
134
+ FileUtils.mkdir_p(public_output_path)
135
+ Dir.chdir(public_output_path) do
136
+ Rake.sh "tar -xzf #{zipped_bundles_filepath}"
137
+ end
138
+
139
+ ReactOnRailsPro::Utils.rorp_puts "gunzipped bundle cache: #{zipped_bundles_filepath} to #{public_output_path}"
140
+
141
+ extract_extra_files_from_cache_dir
142
+ end
143
+ result
144
+ end
145
+
146
+ def extra_files_path
147
+ Rails.root.join(Shakapacker.config.public_output_path, "extra_files")
148
+ end
149
+
150
+ def copy_extra_files_to_cache_dir
151
+ return unless remote_bundle_cache_adapter.respond_to?(:extra_files_to_cache)
152
+
153
+ FileUtils.mkdir_p(extra_files_path)
154
+ copied_extra_files_paths = []
155
+
156
+ remote_bundle_cache_adapter.extra_files_to_cache.each do |file_path|
157
+ if file_path.file?
158
+ copy_file_to_extra_files_cache_dir(file_path)
159
+ copied_extra_files_paths.push(file_path.relative_path_from(Rails.root).to_s)
160
+ else
161
+ ReactOnRailsPro::Utils.rorp_puts "Extra file: #{file_path}, doesn't exist. Skipping"
162
+ end
163
+ end
164
+
165
+ ReactOnRailsPro::Utils.rorp_puts "Copied extra files: #{copied_extra_files_paths.join(', ')} " \
166
+ "to extra_files cache dir"
167
+ end
168
+
169
+ def copy_file_to_extra_files_cache_dir(source_path)
170
+ destination_file_path = convert_to_destination(source_path)
171
+ FileUtils.cp(source_path, destination_file_path)
172
+ end
173
+
174
+ def convert_to_destination(source)
175
+ new_file_name = source.relative_path_from(Rails.root).each_filename.to_a.join("---")
176
+ extra_files_path.join(new_file_name)
177
+ end
178
+
179
+ def extract_extra_files_from_cache_dir
180
+ return unless File.exist?(extra_files_path)
181
+
182
+ extracted_extra_files_paths = []
183
+ Dir.each_child(extra_files_path) do |file_name|
184
+ file_path_parts = file_name.split("---")
185
+ source_file_path = extra_files_path.join(file_name)
186
+ destination_file_path = Rails.root.join(*file_path_parts)
187
+ FileUtils.mv(source_file_path, destination_file_path)
188
+ extracted_extra_files_paths.push(destination_file_path.relative_path_from(Rails.root).to_s)
189
+ end
190
+
191
+ ReactOnRailsPro::Utils.rorp_puts "Extracted extra files: #{extracted_extra_files_paths.join(', ')} " \
192
+ "from extra_files cache dir"
193
+ remove_extra_files_cache_dir
194
+ end
195
+
196
+ def cache_bundles
197
+ begin
198
+ copy_extra_files_to_cache_dir
199
+ public_output_path = Shakapacker.config.public_output_path
200
+ ReactOnRailsPro::Utils.rorp_puts "Gzipping built bundles to #{zipped_bundles_filepath} with " \
201
+ "files in #{public_output_path}"
202
+ Dir.chdir(public_output_path) do
203
+ Rake.sh "tar -czf #{zipped_bundles_filepath} --auto-compress -C " \
204
+ "#{Shakapacker.config.public_output_path} ."
205
+ end
206
+ rescue StandardError => e
207
+ ReactOnRailsPro::Utils.rorp_puts "An error occurred while attempting to zip the built bundles."
208
+ ReactOnRailsPro::Utils.rorp_puts e.message
209
+ puts e.backtrace.join('\n')
210
+ ensure
211
+ remove_extra_files_cache_dir
212
+ end
213
+
214
+ ReactOnRailsPro::Utils.rorp_puts "Bundles will be uploaded to remote bundle cache as #{zipped_bundles_filename}"
215
+
216
+ begin
217
+ remote_bundle_cache_adapter.upload(zipped_bundles_filepath)
218
+ rescue RuntimeError
219
+ message = "An error was raised by the custom config.remote_bundle_cache_adapter 'upload' " \
220
+ "method when called with zipped_bundles_filepath: #{zipped_bundles_filepath}"
221
+ ReactOnRailsPro::Utils.rorp_puts message
222
+ raise
223
+ end
224
+ end
225
+
226
+ def remove_extra_files_cache_dir
227
+ FileUtils.rm_f(extra_files_path)
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "react_on_rails/utils"
4
+
5
+ module ReactOnRailsPro
6
+ class Cache
7
+ class << self
8
+ # options[:cache_options] can include :compress, :expires_in, :race_condition_ttl and
9
+ # other options
10
+ def fetch_react_component(component_name, options)
11
+ if use_cache?(options)
12
+ cache_key = react_component_cache_key(component_name, options)
13
+ Rails.logger.debug { "React on Rails Pro cache_key is #{cache_key.inspect}" }
14
+ cache_options = options[:cache_options]
15
+ cache_hit = true
16
+ result = Rails.cache.fetch(cache_key, cache_options) do
17
+ cache_hit = false
18
+ yield
19
+ end
20
+ # Pass back the cache key in the results only if the result is a Hash
21
+ if result.is_a?(Hash)
22
+ result[:RORP_CACHE_KEY] = cache_key
23
+ result[:RORP_CACHE_HIT] = cache_hit
24
+ end
25
+ result
26
+ else
27
+ yield
28
+ end
29
+ end
30
+
31
+ def use_cache?(options)
32
+ if options.key?(:if)
33
+ options[:if]
34
+ elsif options.key?(:unless)
35
+ !options[:unless]
36
+ else
37
+ true
38
+ end
39
+ end
40
+
41
+ # Cache keys by React on Rails Pro should build upon this base
42
+ # Provide prerender: true in order to include bundle hash in the list of keys.
43
+ # The bundle hash is necessary so that any changes to the bundle fault the cache.
44
+ def base_cache_key(type, prerender: nil)
45
+ keys = [
46
+ type,
47
+ ReactOnRails::VERSION,
48
+ ReactOnRailsPro::VERSION
49
+ ]
50
+
51
+ # TODO: Move comment over to test
52
+ # We only care about the bundle hash if prerendering because we're not caching anything
53
+ # that would be generated by the bundle.
54
+ keys.push(ReactOnRailsPro::Utils.bundle_hash) if prerender
55
+ keys
56
+ end
57
+
58
+ def dependencies_cache_key
59
+ # https://github.com/shakacode/react_on_rails_pro/issues/32
60
+ # https://github.com/shakacode/react_on_rails/issues/39#issuecomment-143472325
61
+ return @dependency_checksum if @dependency_checksum.present? && !Rails.env.development?
62
+ return nil unless ReactOnRailsPro.configuration.dependency_globs.present?
63
+
64
+ @dependency_checksum =
65
+ ReactOnRailsPro::Utils.digest_of_globs(
66
+ ReactOnRailsPro.configuration.dependency_globs
67
+ ).hexdigest
68
+ end
69
+
70
+ def react_component_cache_key(component_name, options)
71
+ cache_key_option = options[:cache_key]
72
+ cache_key_value = if cache_key_option.respond_to?(:call)
73
+ cache_key_option.call
74
+ else
75
+ cache_key_option
76
+ end
77
+
78
+ # NOTE: Rails seems to do this automatically: ActiveSupport::Cache.expand_cache_key(keys)
79
+ [
80
+ *base_cache_key("ror_component", prerender: options[:prerender]),
81
+ dependencies_cache_key,
82
+ component_name,
83
+ cache_key_value
84
+ ].compact
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReactOnRailsPro
4
+ module RSCPayloadRenderer
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ include ReactOnRails::Controller
9
+ include ReactOnRailsPro::Stream
10
+ end
11
+
12
+ def rsc_payload
13
+ @rsc_payload_component_name = rsc_payload_component_name
14
+ @rsc_payload_component_props = rsc_payload_component_props
15
+
16
+ stream_view_containing_react_components(
17
+ template: custom_rsc_payload_template,
18
+ layout: false
19
+ )
20
+ end
21
+
22
+ private
23
+
24
+ def rsc_payload_component_props
25
+ return {} if params[:props].blank?
26
+
27
+ JSON.parse(params[:props])
28
+ end
29
+
30
+ def rsc_payload_component_name
31
+ params[:component_name]
32
+ end
33
+
34
+ def custom_rsc_payload_template
35
+ "react_on_rails_pro/rsc_payload"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReactOnRailsPro
4
+ module Stream
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ include ActionController::Live
9
+ end
10
+
11
+ # Streams React components within a specified template to the client.
12
+ #
13
+ # @param template [String] The path to the template file to be streamed.
14
+ # @param close_stream_at_end [Boolean] Whether to automatically close the stream after rendering (default: true).
15
+ # @param render_options [Hash] Additional options to pass to `render_to_string`.
16
+ #
17
+ # components must be added to the view using the `stream_react_component` helper.
18
+ #
19
+ # @example
20
+ # stream_view_containing_react_components(template: 'path/to/your/template')
21
+ #
22
+ # @example
23
+ # stream_view_containing_react_components(
24
+ # template: 'path/to/your/template',
25
+ # close_stream_at_end: false,
26
+ # layout: false
27
+ # )
28
+ #
29
+ # @note The `stream_react_component` helper is defined in the react_on_rails gem.
30
+ # For more details, refer to `lib/react_on_rails/helper.rb` in the react_on_rails repository.
31
+ #
32
+ # @see ReactOnRails::Helper#stream_react_component
33
+ def stream_view_containing_react_components(template:, close_stream_at_end: true, **render_options)
34
+ @rorp_rendering_fibers = []
35
+ template_string = render_to_string(template: template, **render_options)
36
+ # View may contain extra newlines, chunk already contains a newline
37
+ # Having multiple newlines between chunks causes hydration errors
38
+ # So we strip extra newlines from the template string and add a single newline
39
+ response.stream.write(template_string)
40
+
41
+ begin
42
+ drain_streams_concurrently
43
+ ensure
44
+ response.stream.close if close_stream_at_end
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def drain_streams_concurrently
51
+ require "async"
52
+ require "async/limited_queue"
53
+
54
+ return if @rorp_rendering_fibers.empty?
55
+
56
+ Sync do |parent|
57
+ # To avoid memory bloat, we use a limited queue to buffer chunks in memory.
58
+ buffer_size = ReactOnRailsPro.configuration.concurrent_component_streaming_buffer_size
59
+ queue = Async::LimitedQueue.new(buffer_size)
60
+
61
+ writer = build_writer_task(parent: parent, queue: queue)
62
+ tasks = build_producer_tasks(parent: parent, queue: queue)
63
+
64
+ # This structure ensures that even if a producer task fails, we always
65
+ # signal the writer to stop and then wait for it to finish draining
66
+ # any remaining items from the queue before propagating the error.
67
+ begin
68
+ tasks.each(&:wait)
69
+ ensure
70
+ # `close` signals end-of-stream; when writer tries to dequeue, it will get nil, so it will exit.
71
+ queue.close
72
+ writer.wait
73
+ end
74
+ end
75
+ end
76
+
77
+ def build_producer_tasks(parent:, queue:)
78
+ @rorp_rendering_fibers.each_with_index.map do |fiber, idx|
79
+ parent.async do
80
+ loop do
81
+ chunk = fiber.resume
82
+ break unless chunk
83
+
84
+ # Will be blocked if the queue is full until a chunk is dequeued
85
+ queue.enqueue([idx, chunk])
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ def build_writer_task(parent:, queue:)
92
+ parent.async do
93
+ loop do
94
+ pair = queue.dequeue
95
+ break if pair.nil?
96
+
97
+ _idx_from_queue, item = pair
98
+ response.stream.write(item)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReactOnRailsPro
4
+ def self.configure
5
+ yield(configuration)
6
+ configuration.setup_config_values
7
+ end
8
+
9
+ def self.configuration
10
+ @configuration ||= Configuration.new(
11
+ prerender_caching: Configuration::DEFAULT_PRERENDER_CACHING,
12
+ server_renderer: Configuration::DEFAULT_RENDERER_METHOD,
13
+ renderer_url: Configuration::DEFAULT_RENDERER_URL,
14
+ renderer_use_fallback_exec_js: Configuration::DEFAULT_RENDERER_FALLBACK_EXEC_JS,
15
+ renderer_http_pool_size: Configuration::DEFAULT_RENDERER_HTTP_POOL_SIZE,
16
+ renderer_http_pool_timeout: Configuration::DEFAULT_RENDERER_HTTP_POOL_TIMEOUT,
17
+ renderer_http_pool_warn_timeout: Configuration::DEFAULT_RENDERER_HTTP_POOL_WARN_TIMEOUT,
18
+ renderer_password: nil,
19
+ tracing: Configuration::DEFAULT_TRACING,
20
+ dependency_globs: Configuration::DEFAULT_DEPENDENCY_GLOBS,
21
+ excluded_dependency_globs: Configuration::DEFAULT_EXCLUDED_DEPENDENCY_GLOBS,
22
+ remote_bundle_cache_adapter: Configuration::DEFAULT_REMOTE_BUNDLE_CACHE_ADAPTER,
23
+ ssr_timeout: Configuration::DEFAULT_SSR_TIMEOUT,
24
+ ssr_pre_hook_js: nil,
25
+ assets_to_copy: nil,
26
+ renderer_request_retry_limit: Configuration::DEFAULT_RENDERER_REQUEST_RETRY_LIMIT,
27
+ throw_js_errors: Configuration::DEFAULT_THROW_JS_ERRORS,
28
+ rendering_returns_promises: Configuration::DEFAULT_RENDERING_RETURNS_PROMISES,
29
+ profile_server_rendering_js_code: Configuration::DEFAULT_PROFILE_SERVER_RENDERING_JS_CODE,
30
+ raise_non_shell_server_rendering_errors: Configuration::DEFAULT_RAISE_NON_SHELL_SERVER_RENDERING_ERRORS,
31
+ enable_rsc_support: Configuration::DEFAULT_ENABLE_RSC_SUPPORT,
32
+ rsc_payload_generation_url_path: Configuration::DEFAULT_RSC_PAYLOAD_GENERATION_URL_PATH,
33
+ rsc_bundle_js_file: Configuration::DEFAULT_RSC_BUNDLE_JS_FILE,
34
+ react_client_manifest_file: Configuration::DEFAULT_REACT_CLIENT_MANIFEST_FILE,
35
+ react_server_client_manifest_file: Configuration::DEFAULT_REACT_SERVER_CLIENT_MANIFEST_FILE,
36
+ concurrent_component_streaming_buffer_size: Configuration::DEFAULT_CONCURRENT_COMPONENT_STREAMING_BUFFER_SIZE
37
+ )
38
+ end
39
+
40
+ class Configuration # rubocop:disable Metrics/ClassLength
41
+ DEFAULT_RENDERER_URL = "http://localhost:3800"
42
+ DEFAULT_RENDERER_METHOD = "ExecJS"
43
+ DEFAULT_RENDERER_FALLBACK_EXEC_JS = true
44
+ DEFAULT_RENDERER_HTTP_POOL_SIZE = 10
45
+ DEFAULT_RENDERER_HTTP_POOL_TIMEOUT = 5
46
+ DEFAULT_RENDERER_HTTP_POOL_WARN_TIMEOUT = 0.25
47
+ DEFAULT_SSR_TIMEOUT = 5
48
+ DEFAULT_PRERENDER_CACHING = false
49
+ DEFAULT_TRACING = false
50
+ DEFAULT_DEPENDENCY_GLOBS = [].freeze
51
+ DEFAULT_EXCLUDED_DEPENDENCY_GLOBS = [].freeze
52
+ DEFAULT_REMOTE_BUNDLE_CACHE_ADAPTER = nil
53
+ DEFAULT_RENDERER_REQUEST_RETRY_LIMIT = 5
54
+ DEFAULT_THROW_JS_ERRORS = true
55
+ DEFAULT_RENDERING_RETURNS_PROMISES = false
56
+ DEFAULT_PROFILE_SERVER_RENDERING_JS_CODE = false
57
+ DEFAULT_RAISE_NON_SHELL_SERVER_RENDERING_ERRORS = false
58
+ DEFAULT_ENABLE_RSC_SUPPORT = false
59
+ DEFAULT_RSC_PAYLOAD_GENERATION_URL_PATH = "rsc_payload/"
60
+ DEFAULT_RSC_BUNDLE_JS_FILE = "rsc-bundle.js"
61
+ DEFAULT_REACT_CLIENT_MANIFEST_FILE = "react-client-manifest.json"
62
+ DEFAULT_REACT_SERVER_CLIENT_MANIFEST_FILE = "react-server-client-manifest.json"
63
+ DEFAULT_CONCURRENT_COMPONENT_STREAMING_BUFFER_SIZE = 64
64
+
65
+ attr_accessor :renderer_url, :renderer_password, :tracing,
66
+ :server_renderer, :renderer_use_fallback_exec_js, :prerender_caching,
67
+ :renderer_http_pool_size, :renderer_http_pool_timeout, :renderer_http_pool_warn_timeout,
68
+ :dependency_globs, :excluded_dependency_globs, :rendering_returns_promises,
69
+ :remote_bundle_cache_adapter, :ssr_pre_hook_js, :assets_to_copy,
70
+ :renderer_request_retry_limit, :throw_js_errors, :ssr_timeout,
71
+ :profile_server_rendering_js_code, :raise_non_shell_server_rendering_errors, :enable_rsc_support,
72
+ :rsc_payload_generation_url_path, :rsc_bundle_js_file, :react_client_manifest_file,
73
+ :react_server_client_manifest_file, :concurrent_component_streaming_buffer_size
74
+
75
+ def initialize(renderer_url: nil, renderer_password: nil, server_renderer: nil, # rubocop:disable Metrics/AbcSize
76
+ renderer_use_fallback_exec_js: nil, prerender_caching: nil,
77
+ renderer_http_pool_size: nil, renderer_http_pool_timeout: nil,
78
+ renderer_http_pool_warn_timeout: nil, tracing: nil,
79
+ dependency_globs: nil, excluded_dependency_globs: nil, rendering_returns_promises: nil,
80
+ remote_bundle_cache_adapter: nil, ssr_pre_hook_js: nil, assets_to_copy: nil,
81
+ renderer_request_retry_limit: nil, throw_js_errors: nil, ssr_timeout: nil,
82
+ profile_server_rendering_js_code: nil, raise_non_shell_server_rendering_errors: nil,
83
+ enable_rsc_support: nil, rsc_payload_generation_url_path: nil,
84
+ rsc_bundle_js_file: nil, react_client_manifest_file: nil,
85
+ react_server_client_manifest_file: nil,
86
+ concurrent_component_streaming_buffer_size: DEFAULT_CONCURRENT_COMPONENT_STREAMING_BUFFER_SIZE)
87
+ self.renderer_url = renderer_url
88
+ self.renderer_password = renderer_password
89
+ self.server_renderer = server_renderer
90
+ self.renderer_use_fallback_exec_js = renderer_use_fallback_exec_js
91
+ self.prerender_caching = prerender_caching
92
+ self.renderer_http_pool_size = renderer_http_pool_size
93
+ self.renderer_http_pool_timeout = renderer_http_pool_timeout
94
+ self.renderer_http_pool_warn_timeout = renderer_http_pool_warn_timeout
95
+ self.tracing = tracing
96
+ self.rendering_returns_promises = server_renderer == "NodeRenderer" ? rendering_returns_promises : false
97
+ self.dependency_globs = dependency_globs
98
+ self.excluded_dependency_globs = excluded_dependency_globs
99
+ self.remote_bundle_cache_adapter = remote_bundle_cache_adapter
100
+ self.ssr_pre_hook_js = ssr_pre_hook_js
101
+ self.assets_to_copy = assets_to_copy
102
+ self.renderer_request_retry_limit = renderer_request_retry_limit
103
+ self.throw_js_errors = throw_js_errors
104
+ self.ssr_timeout = ssr_timeout
105
+ self.profile_server_rendering_js_code = profile_server_rendering_js_code
106
+ self.raise_non_shell_server_rendering_errors = raise_non_shell_server_rendering_errors
107
+ self.enable_rsc_support = enable_rsc_support
108
+ self.rsc_payload_generation_url_path = rsc_payload_generation_url_path
109
+ self.rsc_bundle_js_file = rsc_bundle_js_file
110
+ self.react_client_manifest_file = react_client_manifest_file
111
+ self.react_server_client_manifest_file = react_server_client_manifest_file
112
+ self.concurrent_component_streaming_buffer_size = concurrent_component_streaming_buffer_size
113
+ end
114
+
115
+ def setup_config_values
116
+ configure_default_url_if_not_provided
117
+ validate_url
118
+ validate_remote_bundle_cache_adapter
119
+ setup_renderer_password
120
+ setup_assets_to_copy
121
+ validate_concurrent_component_streaming_buffer_size
122
+ setup_execjs_profiler_if_needed
123
+ check_react_on_rails_support_for_rsc
124
+ end
125
+
126
+ def check_react_on_rails_support_for_rsc
127
+ return unless enable_rsc_support
128
+
129
+ return if ReactOnRails::Utils.respond_to?(:rsc_support_enabled?)
130
+
131
+ raise ReactOnRailsPro::Error, <<~MSG
132
+ React Server Components (RSC) support requires react_on_rails version 15.0.0 or higher.
133
+ Please upgrade your react_on_rails gem to enable this feature.
134
+ MSG
135
+ end
136
+
137
+ def setup_execjs_profiler_if_needed
138
+ return unless profile_server_rendering_js_code && server_renderer == "ExecJS"
139
+
140
+ if ExecJS.runtime == ExecJS::Runtimes::Node
141
+ ExecJS.runtime = ExecJS::ExternalRuntime.new(
142
+ name: "Node.js (V8)",
143
+ command: ["node --prof"],
144
+ runner_path: "#{ExecJS.root}/support/node_runner.js",
145
+ encoding: "UTF-8"
146
+ )
147
+ elsif ExecJS.runtime == ExecJS::Runtimes::V8
148
+ ExecJS.runtime = ExecJS::ExternalRuntime.new(
149
+ name: "V8",
150
+ command: ["d8 --prof"],
151
+ runner_path: "#{ExecJS.root}/support/v8_runner.js",
152
+ encoding: "UTF-8"
153
+ )
154
+ else
155
+ current_runtime = ExecJS.runtime.name
156
+ message = <<~MSG
157
+ You have set `profile_server_rendering_js_code` to true, but the current execjs runtime is #{current_runtime}.
158
+ ExecJS profiler only supports Node.js (V8) or V8 runtimes.
159
+ You can set the runtime by setting the `EXECJS_RUNTIME` environment variable to either `Node` or `V8`.
160
+ MSG
161
+ raise ReactOnRailsPro::Error, message
162
+ end
163
+ end
164
+
165
+ def node_renderer?
166
+ ReactOnRailsPro.configuration.server_renderer == "NodeRenderer"
167
+ end
168
+
169
+ private
170
+
171
+ def setup_assets_to_copy
172
+ self.assets_to_copy = (Array(assets_to_copy) if assets_to_copy.present?)
173
+ end
174
+
175
+ def configure_default_url_if_not_provided
176
+ self.renderer_url = renderer_url.presence || DEFAULT_RENDERER_URL
177
+ end
178
+
179
+ def validate_url
180
+ URI(renderer_url)
181
+ rescue URI::InvalidURIError => e
182
+ message = "Unparseable ReactOnRailsPro.config.renderer_url #{renderer_url} provided.\n" \
183
+ "#{e.message}"
184
+ raise ReactOnRailsPro::Error, message
185
+ end
186
+
187
+ def validate_remote_bundle_cache_adapter
188
+ if !remote_bundle_cache_adapter.nil? && !remote_bundle_cache_adapter.is_a?(Module)
189
+ raise ReactOnRailsPro::Error, "config.remote_bundle_cache_adapter can only have a module or class assigned"
190
+ end
191
+
192
+ return unless remote_bundle_cache_adapter.is_a?(Module)
193
+
194
+ unless remote_bundle_cache_adapter.methods.include?(:build)
195
+ raise ReactOnRailsPro::Error,
196
+ "config.remote_bundle_cache_adapter must have a class method named 'build'"
197
+ end
198
+
199
+ unless remote_bundle_cache_adapter.methods.include?(:fetch)
200
+ raise ReactOnRailsPro::Error,
201
+ "config.remote_bundle_cache_adapter must have a class method named 'fetch'" \
202
+ "which takes a single named String parameter 'zipped_bundles_filename'" \
203
+ "and returns the zipped file as a string if fetch attempt is successful & nil if not"
204
+ end
205
+
206
+ unless remote_bundle_cache_adapter.methods.include?(:upload) # rubocop:disable Style/GuardClause
207
+ raise ReactOnRailsPro::Error,
208
+ "config.remote_bundle_cache_adapter must have a class method named 'upload'" \
209
+ "which takes a single named Pathname parameter 'zipped_bundles_filepath' & returns nil"
210
+ end
211
+ end
212
+
213
+ def validate_concurrent_component_streaming_buffer_size
214
+ return if concurrent_component_streaming_buffer_size.is_a?(Integer) &&
215
+ concurrent_component_streaming_buffer_size.positive?
216
+
217
+ raise ReactOnRailsPro::Error,
218
+ "config.concurrent_component_streaming_buffer_size must be a positive integer"
219
+ end
220
+
221
+ def setup_renderer_password
222
+ return if renderer_password.present?
223
+
224
+ uri = URI(renderer_url)
225
+ self.renderer_password = uri.password
226
+ end
227
+ end
228
+ end