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.
- checksums.yaml +7 -0
- data/.controlplane/Dockerfile +49 -0
- data/.controlplane/controlplane.yml +22 -0
- data/.controlplane/gvc.yml +25 -0
- data/.controlplane/postgres.yml +33 -0
- data/.controlplane/rails.yml +49 -0
- data/.controlplane/redis.yml +18 -0
- data/.gitignore +77 -0
- data/.prettierignore +12 -0
- data/.prettierrc +19 -0
- data/.rspec +2 -0
- data/.rubocop.yml +120 -0
- data/.scss-lint.yml +205 -0
- data/CHANGELOG.md +570 -0
- data/CI_SETUP.md +502 -0
- data/CONTRIBUTING.md +376 -0
- data/Dockerfile +63 -0
- data/Gemfile +8 -0
- data/Gemfile.development_dependencies +74 -0
- data/Gemfile.loader +32 -0
- data/Gemfile.lock +527 -0
- data/LICENSE +98 -0
- data/LICENSE_SETUP.md +272 -0
- data/README.md +577 -0
- data/Rakefile +13 -0
- data/app/controllers/react_on_rails_pro/rsc_payload_controller.rb +7 -0
- data/app/helpers/react_on_rails_pro_helper.rb +360 -0
- data/app/views/react_on_rails_pro/rsc_payload.html.erb +1 -0
- data/babel.config.js +4 -0
- data/docs/bundle-caching.md +205 -0
- data/docs/caching.md +234 -0
- data/docs/code-splitting-loadable-components.md +313 -0
- data/docs/code-splitting.md +349 -0
- data/docs/configuration.md +165 -0
- data/docs/contributors-info/onboarding-customers.md +6 -0
- data/docs/contributors-info/releasing.md +40 -0
- data/docs/contributors-info/style.md +33 -0
- data/docs/home-pro.md +146 -0
- data/docs/installation.md +203 -0
- data/docs/js-memory-leaks.md +22 -0
- data/docs/node-renderer/basics.md +92 -0
- data/docs/node-renderer/debugging.md +38 -0
- data/docs/node-renderer/error-reporting-and-tracing.md +160 -0
- data/docs/node-renderer/heroku.md +102 -0
- data/docs/node-renderer/js-configuration.md +91 -0
- data/docs/node-renderer/troubleshooting.md +5 -0
- data/docs/profiling-server-side-rendering-code.md +179 -0
- data/docs/react-server-components/add-streaming-and-interactivity.md +190 -0
- data/docs/react-server-components/create-without-ssr.md +448 -0
- data/docs/react-server-components/glossary.md +102 -0
- data/docs/react-server-components/how-react-server-components-work.md +243 -0
- data/docs/react-server-components/inside-client-components.md +332 -0
- data/docs/react-server-components/purpose-and-benefits.md +243 -0
- data/docs/react-server-components/rendering-flow.md +86 -0
- data/docs/react-server-components/selective-hydration-in-streamed-components.md +75 -0
- data/docs/react-server-components/server-side-rendering.md +72 -0
- data/docs/react-server-components/tutorial.md +19 -0
- data/docs/release-notes/4.0.md +94 -0
- data/docs/release-notes/v4-react-server-components.md +66 -0
- data/docs/ruby-api.md +11 -0
- data/docs/streaming-server-rendering.md +210 -0
- data/docs/troubleshooting.md +24 -0
- data/docs/updating.md +219 -0
- data/eslint.config.mjs +220 -0
- data/lib/react_on_rails_pro/assets_precompile.rb +230 -0
- data/lib/react_on_rails_pro/cache.rb +88 -0
- data/lib/react_on_rails_pro/concerns/rsc_payload_renderer.rb +38 -0
- data/lib/react_on_rails_pro/concerns/stream.rb +103 -0
- data/lib/react_on_rails_pro/configuration.rb +228 -0
- data/lib/react_on_rails_pro/constants.rb +8 -0
- data/lib/react_on_rails_pro/engine.rb +24 -0
- data/lib/react_on_rails_pro/error.rb +14 -0
- data/lib/react_on_rails_pro/license_public_key.rb +30 -0
- data/lib/react_on_rails_pro/license_validator.rb +188 -0
- data/lib/react_on_rails_pro/prepare_node_renderer_bundles.rb +40 -0
- data/lib/react_on_rails_pro/rendering_error.rb +5 -0
- data/lib/react_on_rails_pro/request.rb +318 -0
- data/lib/react_on_rails_pro/routes.rb +13 -0
- data/lib/react_on_rails_pro/server_rendering_js_code.rb +102 -0
- data/lib/react_on_rails_pro/server_rendering_pool/node_rendering_pool.rb +133 -0
- data/lib/react_on_rails_pro/server_rendering_pool/pro_rendering.rb +117 -0
- data/lib/react_on_rails_pro/stream_cache.rb +61 -0
- data/lib/react_on_rails_pro/stream_request.rb +170 -0
- data/lib/react_on_rails_pro/utils.rb +222 -0
- data/lib/react_on_rails_pro/v8_log_processor.rb +50 -0
- data/lib/react_on_rails_pro/version.rb +6 -0
- data/lib/react_on_rails_pro.rb +23 -0
- data/package-scripts.yml +109 -0
- data/package.json +159 -0
- data/rakelib/dummy_apps.rake +22 -0
- data/rakelib/lint.rake +32 -0
- data/rakelib/public_key_management.rake +155 -0
- data/rakelib/rbs.rake +47 -0
- data/rakelib/run_rspec.rake +81 -0
- data/rakelib/task_helpers.rb +45 -0
- data/rakelib/yard.rake +20 -0
- data/react_on_rails_pro.gemspec +47 -0
- data/readme-gen-docs.md +1 -0
- data/script/bootstrap +33 -0
- data/script/preinstall.js +31 -0
- data/script/setup +23 -0
- data/script/test +38 -0
- data/sig/react_on_rails_pro/cache.rbs +13 -0
- data/sig/react_on_rails_pro/configuration.rbs +100 -0
- data/sig/react_on_rails_pro/error.rbs +4 -0
- data/sig/react_on_rails_pro/utils.rbs +7 -0
- data/sig/react_on_rails_pro.rbs +5 -0
- data/yarn.lock +7599 -0
- 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
|