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,360 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# NOTE: For any heredoc JS:
|
|
4
|
+
# 1. The white spacing in this file matters!
|
|
5
|
+
# 2. Keep all #{some_var} fully to the left so that all indentation is done evenly in that var
|
|
6
|
+
|
|
7
|
+
require "react_on_rails/helper"
|
|
8
|
+
|
|
9
|
+
# rubocop:disable Metrics/ModuleLength
|
|
10
|
+
module ReactOnRailsProHelper
|
|
11
|
+
def fetch_react_component(component_name, options)
|
|
12
|
+
if ReactOnRailsPro::Cache.use_cache?(options)
|
|
13
|
+
cache_key = ReactOnRailsPro::Cache.react_component_cache_key(component_name, options)
|
|
14
|
+
Rails.logger.debug { "React on Rails Pro cache_key is #{cache_key.inspect}" }
|
|
15
|
+
cache_options = options[:cache_options]
|
|
16
|
+
cache_hit = true
|
|
17
|
+
result = Rails.cache.fetch(cache_key, cache_options) do
|
|
18
|
+
cache_hit = false
|
|
19
|
+
yield
|
|
20
|
+
end
|
|
21
|
+
if cache_hit
|
|
22
|
+
render_options = ReactOnRails::ReactComponent::RenderOptions.new(
|
|
23
|
+
react_component_name: component_name,
|
|
24
|
+
options: options
|
|
25
|
+
)
|
|
26
|
+
load_pack_for_generated_component(component_name, render_options)
|
|
27
|
+
end
|
|
28
|
+
# Pass back the cache key in the results only if the result is a Hash
|
|
29
|
+
if result.is_a?(Hash)
|
|
30
|
+
result[:RORP_CACHE_KEY] = cache_key
|
|
31
|
+
result[:RORP_CACHE_HIT] = cache_hit
|
|
32
|
+
end
|
|
33
|
+
result
|
|
34
|
+
else
|
|
35
|
+
yield
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Provide caching support for react_component in a manner akin to Rails fragment caching.
|
|
40
|
+
# All the same options as react_component apply with the following difference:
|
|
41
|
+
#
|
|
42
|
+
# 1. You must pass the props as a block. This is so that the evaluation of the props is not done
|
|
43
|
+
# if the cache can be used.
|
|
44
|
+
# 2. Provide the cache_key option
|
|
45
|
+
# cache_key: String or Array (or Proc returning a String or Array) containing your cache keys.
|
|
46
|
+
# If prerender is set to true, the server bundle digest will be included in the cache key.
|
|
47
|
+
# The cache_key value is the same as used for conventional Rails fragment caching.
|
|
48
|
+
# 3. Optionally provide the `:cache_options` key with a value of a hash including as
|
|
49
|
+
# :compress, :expires_in, :race_condition_ttl as documented in the Rails Guides
|
|
50
|
+
# 4. Provide boolean values for `:if` or `:unless` to conditionally use caching.
|
|
51
|
+
def cached_react_component(component_name, raw_options = {}, &block)
|
|
52
|
+
ReactOnRailsPro::Utils.with_trace(component_name) do
|
|
53
|
+
check_caching_options!(raw_options, block)
|
|
54
|
+
|
|
55
|
+
fetch_react_component(component_name, raw_options) do
|
|
56
|
+
sanitized_options = raw_options
|
|
57
|
+
sanitized_options[:props] = yield
|
|
58
|
+
sanitized_options[:skip_prerender_cache] = true
|
|
59
|
+
sanitized_options[:auto_load_bundle] =
|
|
60
|
+
ReactOnRails.configuration.auto_load_bundle || raw_options[:auto_load_bundle]
|
|
61
|
+
react_component(component_name, sanitized_options)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Provide caching support for react_component_hash in a manner akin to Rails fragment caching.
|
|
67
|
+
# All the same options as react_component_hash apply with the following difference:
|
|
68
|
+
#
|
|
69
|
+
# 1. You must pass the props as a block. This is so that the evaluation of the props is not done
|
|
70
|
+
# if the cache can be used.
|
|
71
|
+
# 2. Provide the cache_key option
|
|
72
|
+
# cache_key: String or Array (or Proc returning a String or Array) containing your cache keys.
|
|
73
|
+
# Since prerender is automatically set to true, the server bundle digest will be included in the cache key.
|
|
74
|
+
# The cache_key value is the same as used for conventional Rails fragment caching.
|
|
75
|
+
# 3. Optionally provide the `:cache_options` key with a value of a hash including as
|
|
76
|
+
# :compress, :expires_in, :race_condition_ttl as documented in the Rails Guides
|
|
77
|
+
# 4. Provide boolean values for `:if` or `:unless` to conditionally use caching.
|
|
78
|
+
def cached_react_component_hash(component_name, raw_options = {}, &block)
|
|
79
|
+
raw_options[:prerender] = true
|
|
80
|
+
|
|
81
|
+
ReactOnRailsPro::Utils.with_trace(component_name) do
|
|
82
|
+
check_caching_options!(raw_options, block)
|
|
83
|
+
|
|
84
|
+
fetch_react_component(component_name, raw_options) do
|
|
85
|
+
sanitized_options = raw_options
|
|
86
|
+
sanitized_options[:props] = yield
|
|
87
|
+
sanitized_options[:skip_prerender_cache] = true
|
|
88
|
+
sanitized_options[:auto_load_bundle] =
|
|
89
|
+
ReactOnRails.configuration.auto_load_bundle || raw_options[:auto_load_bundle]
|
|
90
|
+
react_component_hash(component_name, sanitized_options)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Streams a server-side rendered React component using React's `renderToPipeableStream`.
|
|
96
|
+
# Supports React 18 features like Suspense, concurrent rendering, and selective hydration.
|
|
97
|
+
# Enables progressive rendering and improved performance for large components.
|
|
98
|
+
#
|
|
99
|
+
# Note: This function can only be used with React on Rails Pro.
|
|
100
|
+
# The view that uses this function must be rendered using the
|
|
101
|
+
# `stream_view_containing_react_components` method from the React on Rails Pro gem.
|
|
102
|
+
#
|
|
103
|
+
# Example of an async React component that can benefit from streaming:
|
|
104
|
+
#
|
|
105
|
+
# const AsyncComponent = async () => {
|
|
106
|
+
# const data = await fetchData();
|
|
107
|
+
# return <div>{data}</div>;
|
|
108
|
+
# };
|
|
109
|
+
#
|
|
110
|
+
# function App() {
|
|
111
|
+
# return (
|
|
112
|
+
# <Suspense fallback={<div>Loading...</div>}>
|
|
113
|
+
# <AsyncComponent />
|
|
114
|
+
# </Suspense>
|
|
115
|
+
# );
|
|
116
|
+
# }
|
|
117
|
+
#
|
|
118
|
+
# @param [String] component_name Name of your registered component
|
|
119
|
+
# @param [Hash] options Options for rendering
|
|
120
|
+
# @option options [Hash] :props Props to pass to the react component
|
|
121
|
+
# @option options [String] :dom_id DOM ID of the component container
|
|
122
|
+
# @option options [Hash] :html_options Options passed to content_tag
|
|
123
|
+
# @option options [Boolean] :trace Set to true to add extra debugging information to the HTML
|
|
124
|
+
# @option options [Boolean] :raise_on_prerender_error Set to true to raise exceptions during server-side rendering
|
|
125
|
+
# Any other options are passed to the content tag, including the id.
|
|
126
|
+
def stream_react_component(component_name, options = {})
|
|
127
|
+
# stream_react_component doesn't have the prerender option
|
|
128
|
+
# Because setting prerender to false is equivalent to calling react_component with prerender: false
|
|
129
|
+
options[:prerender] = true
|
|
130
|
+
options = options.merge(immediate_hydration: true) unless options.key?(:immediate_hydration)
|
|
131
|
+
run_stream_inside_fiber do
|
|
132
|
+
internal_stream_react_component(component_name, options)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Renders the React Server Component (RSC) payload for a given component. This helper generates
|
|
137
|
+
# a special format designed by React for serializing server components and transmitting them
|
|
138
|
+
# to the client.
|
|
139
|
+
#
|
|
140
|
+
# @return [String] Returns a Newline Delimited JSON (NDJSON) stream where each line contains a JSON object with:
|
|
141
|
+
# - html: The RSC payload containing the rendered server components and client component references
|
|
142
|
+
# - consoleReplayScript: JavaScript to replay server-side console logs in the client
|
|
143
|
+
# - hasErrors: Boolean indicating if any errors occurred during rendering
|
|
144
|
+
# - isShellReady: Boolean indicating if the initial shell is ready for hydration
|
|
145
|
+
#
|
|
146
|
+
# Example NDJSON stream:
|
|
147
|
+
# {"html":"<RSC Payload>","consoleReplayScript":"","hasErrors":false,"isShellReady":true}
|
|
148
|
+
# {"html":"<RSC Payload>","consoleReplayScript":"console.log('Loading...')","hasErrors":false,"isShellReady":true}
|
|
149
|
+
#
|
|
150
|
+
# The RSC payload within the html field contains:
|
|
151
|
+
# - The component's rendered output from the server
|
|
152
|
+
# - References to client components that need hydration
|
|
153
|
+
# - Data props passed to client components
|
|
154
|
+
#
|
|
155
|
+
# @param component_name [String] The name of the React component to render. This component should
|
|
156
|
+
# be a server component or a mixed component tree containing both server and client components.
|
|
157
|
+
#
|
|
158
|
+
# @param options [Hash] Options for rendering the component
|
|
159
|
+
# @option options [Hash] :props Props to pass to the component (default: {})
|
|
160
|
+
# @option options [Boolean] :trace Enable tracing for debugging (default: false)
|
|
161
|
+
# @option options [String] :id Custom DOM ID for the component container (optional)
|
|
162
|
+
#
|
|
163
|
+
# @example Basic usage with a server component
|
|
164
|
+
# <%= rsc_payload_react_component("ReactServerComponentPage") %>
|
|
165
|
+
#
|
|
166
|
+
# @example With props and tracing enabled
|
|
167
|
+
# <%= rsc_payload_react_component("RSCPostsPage",
|
|
168
|
+
# props: { artificialDelay: 1000 },
|
|
169
|
+
# trace: true) %>
|
|
170
|
+
#
|
|
171
|
+
# @note This helper requires React Server Components support to be enabled in your configuration:
|
|
172
|
+
# ReactOnRailsPro.configure do |config|
|
|
173
|
+
# config.enable_rsc_support = true
|
|
174
|
+
# end
|
|
175
|
+
#
|
|
176
|
+
# @raise [ReactOnRailsPro::Error] if RSC support is not enabled in configuration
|
|
177
|
+
#
|
|
178
|
+
# @note You don't have to deal directly with this helper function - it's used internally by the
|
|
179
|
+
# `rsc_payload_route` helper function. The returned data from this function is used internally by
|
|
180
|
+
# components registered using the `registerServerComponent` function. Don't use it unless you need
|
|
181
|
+
# more control over the RSC payload generation. To know more about RSC payload, see the following link:
|
|
182
|
+
# @see https://www.shakacode.com/react-on-rails-pro/docs/how-react-server-components-works.md
|
|
183
|
+
# for technical details about the RSC payload format
|
|
184
|
+
def rsc_payload_react_component(component_name, options = {})
|
|
185
|
+
# rsc_payload_react_component doesn't have the prerender option
|
|
186
|
+
# Because setting prerender to false will not do anything
|
|
187
|
+
options[:prerender] = true
|
|
188
|
+
run_stream_inside_fiber do
|
|
189
|
+
internal_rsc_payload_react_component(component_name, options)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Provide caching support for stream_react_component in a manner akin to Rails fragment caching.
|
|
194
|
+
# All the same options as stream_react_component apply with the following differences:
|
|
195
|
+
#
|
|
196
|
+
# 1. You must pass the props as a block. This is so that the evaluation of the props is not done
|
|
197
|
+
# if the cache can be used.
|
|
198
|
+
# 2. Provide the cache_key option
|
|
199
|
+
# cache_key: String or Array (or Proc returning a String or Array) containing your cache keys.
|
|
200
|
+
# Since prerender is automatically set to true, the server bundle digest will be included in the cache key.
|
|
201
|
+
# The cache_key value is the same as used for conventional Rails fragment caching.
|
|
202
|
+
# 3. Optionally provide the `:cache_options` key with a value of a hash including as
|
|
203
|
+
# :compress, :expires_in, :race_condition_ttl as documented in the Rails Guides
|
|
204
|
+
# 4. Provide boolean values for `:if` or `:unless` to conditionally use caching.
|
|
205
|
+
def cached_stream_react_component(component_name, raw_options = {}, &block)
|
|
206
|
+
ReactOnRailsPro::Utils.with_trace(component_name) do
|
|
207
|
+
check_caching_options!(raw_options, block)
|
|
208
|
+
fetch_stream_react_component(component_name, raw_options, &block)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
if defined?(ScoutApm)
|
|
213
|
+
include ScoutApm::Tracer
|
|
214
|
+
instrument_method :cached_react_component, type: "ReactOnRails", name: "cached_react_component"
|
|
215
|
+
instrument_method :cached_react_component_hash, type: "ReactOnRails", name: "cached_react_component_hash"
|
|
216
|
+
instrument_method :cached_stream_react_component, type: "ReactOnRails", name: "cached_stream_react_component"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
private
|
|
220
|
+
|
|
221
|
+
def fetch_stream_react_component(component_name, raw_options, &block)
|
|
222
|
+
auto_load_bundle = ReactOnRails.configuration.auto_load_bundle || raw_options[:auto_load_bundle]
|
|
223
|
+
|
|
224
|
+
unless ReactOnRailsPro::Cache.use_cache?(raw_options)
|
|
225
|
+
return render_stream_component_with_props(component_name, raw_options, auto_load_bundle, &block)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Compose a cache key consistent with non-stream helper semantics.
|
|
229
|
+
key_options = raw_options.merge(prerender: true)
|
|
230
|
+
view_cache_key = ReactOnRailsPro::Cache.react_component_cache_key(component_name, key_options)
|
|
231
|
+
|
|
232
|
+
# Attempt HIT without evaluating props block
|
|
233
|
+
if (cached_chunks = Rails.cache.read(view_cache_key)).is_a?(Array)
|
|
234
|
+
return handle_stream_cache_hit(component_name, raw_options, auto_load_bundle, cached_chunks)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# MISS: evaluate props lazily, stream live, and write-through to view-level cache
|
|
238
|
+
handle_stream_cache_miss(component_name, raw_options, auto_load_bundle, view_cache_key, &block)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def handle_stream_cache_hit(component_name, raw_options, auto_load_bundle, cached_chunks)
|
|
242
|
+
render_options = ReactOnRails::ReactComponent::RenderOptions.new(
|
|
243
|
+
react_component_name: component_name,
|
|
244
|
+
options: { auto_load_bundle: auto_load_bundle }.merge(raw_options)
|
|
245
|
+
)
|
|
246
|
+
load_pack_for_generated_component(component_name, render_options)
|
|
247
|
+
|
|
248
|
+
initial_result, *rest_chunks = cached_chunks
|
|
249
|
+
hit_fiber = Fiber.new do
|
|
250
|
+
rest_chunks.each { |chunk| Fiber.yield(chunk) }
|
|
251
|
+
nil
|
|
252
|
+
end
|
|
253
|
+
@rorp_rendering_fibers << hit_fiber
|
|
254
|
+
initial_result
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def handle_stream_cache_miss(component_name, raw_options, auto_load_bundle, view_cache_key, &block)
|
|
258
|
+
# Kick off the normal streaming helper to get the initial result and the original fiber
|
|
259
|
+
initial_result = render_stream_component_with_props(component_name, raw_options, auto_load_bundle, &block)
|
|
260
|
+
original_fiber = @rorp_rendering_fibers.pop
|
|
261
|
+
|
|
262
|
+
buffered_chunks = [initial_result]
|
|
263
|
+
wrapper_fiber = Fiber.new do
|
|
264
|
+
while (chunk = original_fiber.resume)
|
|
265
|
+
buffered_chunks << chunk
|
|
266
|
+
Fiber.yield(chunk)
|
|
267
|
+
end
|
|
268
|
+
Rails.cache.write(view_cache_key, buffered_chunks, raw_options[:cache_options] || {})
|
|
269
|
+
nil
|
|
270
|
+
end
|
|
271
|
+
@rorp_rendering_fibers << wrapper_fiber
|
|
272
|
+
initial_result
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def render_stream_component_with_props(component_name, raw_options, auto_load_bundle)
|
|
276
|
+
props = yield
|
|
277
|
+
options = raw_options.merge(
|
|
278
|
+
props: props,
|
|
279
|
+
prerender: true,
|
|
280
|
+
skip_prerender_cache: true,
|
|
281
|
+
auto_load_bundle: auto_load_bundle
|
|
282
|
+
)
|
|
283
|
+
stream_react_component(component_name, options)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def check_caching_options!(raw_options, block)
|
|
287
|
+
raise ReactOnRailsPro::Error, "Pass 'props' as a block if using caching" if raw_options.key?(:props) || block.nil?
|
|
288
|
+
|
|
289
|
+
return if raw_options.key?(:cache_key)
|
|
290
|
+
|
|
291
|
+
raise ReactOnRailsPro::Error, "Option 'cache_key' is required for React on Rails caching"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def run_stream_inside_fiber
|
|
295
|
+
if @rorp_rendering_fibers.nil?
|
|
296
|
+
raise ReactOnRails::Error,
|
|
297
|
+
"You must call stream_view_containing_react_components to render the view containing the react component"
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
rendering_fiber = Fiber.new do
|
|
301
|
+
stream = yield
|
|
302
|
+
stream.each_chunk do |chunk|
|
|
303
|
+
Fiber.yield chunk
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
@rorp_rendering_fibers << rendering_fiber
|
|
308
|
+
|
|
309
|
+
# return the first chunk of the fiber
|
|
310
|
+
# It contains the initial html of the component
|
|
311
|
+
# all updates will be appended to the stream sent to browser
|
|
312
|
+
rendering_fiber.resume
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def internal_stream_react_component(component_name, options = {})
|
|
316
|
+
options = options.merge(render_mode: :html_streaming)
|
|
317
|
+
result = internal_react_component(component_name, options)
|
|
318
|
+
build_react_component_result_for_server_streamed_content(
|
|
319
|
+
rendered_html_stream: result[:result],
|
|
320
|
+
component_specification_tag: result[:tag],
|
|
321
|
+
render_options: result[:render_options]
|
|
322
|
+
)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def internal_rsc_payload_react_component(react_component_name, options = {})
|
|
326
|
+
options = options.merge(render_mode: :rsc_payload_streaming)
|
|
327
|
+
render_options = create_render_options(react_component_name, options)
|
|
328
|
+
json_stream = server_rendered_react_component(render_options)
|
|
329
|
+
json_stream.transform do |chunk|
|
|
330
|
+
"#{chunk.to_json}\n".html_safe
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def build_react_component_result_for_server_streamed_content(
|
|
335
|
+
rendered_html_stream:,
|
|
336
|
+
component_specification_tag:,
|
|
337
|
+
render_options:
|
|
338
|
+
)
|
|
339
|
+
is_first_chunk = true
|
|
340
|
+
rendered_html_stream.transform do |chunk_json_result|
|
|
341
|
+
if is_first_chunk
|
|
342
|
+
is_first_chunk = false
|
|
343
|
+
build_react_component_result_for_server_rendered_string(
|
|
344
|
+
server_rendered_html: chunk_json_result["html"],
|
|
345
|
+
component_specification_tag: component_specification_tag,
|
|
346
|
+
console_script: chunk_json_result["consoleReplayScript"],
|
|
347
|
+
render_options: render_options
|
|
348
|
+
)
|
|
349
|
+
else
|
|
350
|
+
result_console_script = render_options.replay_console ? chunk_json_result["consoleReplayScript"] : ""
|
|
351
|
+
# No need to prepend component_specification_tag or add rails context again
|
|
352
|
+
# as they're already included in the first chunk
|
|
353
|
+
compose_react_component_html_with_spec_and_console(
|
|
354
|
+
"", chunk_json_result["html"], result_console_script
|
|
355
|
+
)
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
# rubocop:enable Metrics/ModuleLength
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= rsc_payload_react_component(@rsc_payload_component_name, props: @rsc_payload_component_props) %>
|
data/babel.config.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Bundle Caching
|
|
2
|
+
|
|
3
|
+
## Why?
|
|
4
|
+
Building webpack bundles is often time-consuming, and the same bundles are built many times.
|
|
5
|
+
For example, you might build the production bundles during CI, then for a Review app, then
|
|
6
|
+
for Staging, and maybe even for Production. Or you might want to deploy a small Ruby only
|
|
7
|
+
change to production, but you will have to wait minutes for your bundles to be built again.
|
|
8
|
+
|
|
9
|
+
## Solution
|
|
10
|
+
React on Rails 2.1.0 introduces bundle caching based on a digest of all the source files, defined
|
|
11
|
+
in the `config/shakapacker.yml` file, plus other files defined with `config.dependency_globs` and
|
|
12
|
+
excluding any files from `config.excluded_dependency_globs`. Creating this hash key takes at most a
|
|
13
|
+
few seconds for even large projects. Additionally, the cache key includes
|
|
14
|
+
1. NODE_ENV
|
|
15
|
+
2. Version of React on Rails Pro
|
|
16
|
+
3. Configurable additional env values by supplying an array in method cache_keys on the `remote_bundle_cache_adapter`. See examples below.
|
|
17
|
+
|
|
18
|
+
This cache key is used for saving files to some remote storage, typically S3.
|
|
19
|
+
|
|
20
|
+
## Bonus for local development with multiple directories building production builds
|
|
21
|
+
Bundle caching can help save time if you have multiple directories for the same repository.
|
|
22
|
+
|
|
23
|
+
The bundles are cached in `Rails.root.join('tmp', 'bundle_cache')`
|
|
24
|
+
|
|
25
|
+
So, if you have sibling directories for the same project, you can make a sym link so both directories use the same bundle cache directory.
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
cd my_project2/tmp
|
|
29
|
+
ln -s ../../my_project/tmp/bundle_cache
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
### 1. React on Rails Configuration
|
|
35
|
+
First, we need to tell React on Rails to use a custom build module. In
|
|
36
|
+
`config/initializers/react_on_rails`, set this value:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
config.build_production_command = ReactOnRailsPro::AssetsPrecompile
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Alternatively, if you need to run something after the files are built or extracted from the cache, you can do something like this:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
ReactOnRails.configure do |config|
|
|
46
|
+
# This configures the script to run to build the production assets by webpack. Set this to nil
|
|
47
|
+
# if you don't want react_on_rails building this file for you.
|
|
48
|
+
config.build_production_command = CustomBuildCommand
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
And define it like this:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
module CustomBuildCommand
|
|
56
|
+
def self.call
|
|
57
|
+
ReactOnRailsPro::AssetsPrecompile.call
|
|
58
|
+
Rake::Task['react_on_rails_pro:pre_stage_bundle_for_node_renderer'].invoke
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 2. React on Rails Pro Configuration
|
|
64
|
+
Next, we need to configure the `config/initializers/react_on_rails_pro.rb` with some module,
|
|
65
|
+
say called S3BundleCacheAdapter.
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
config.remote_bundle_cache_adapter = S3BundleCacheAdapter
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This module needs four class methods: `cache_keys` (optional), `build`, `fetch`, `upload`. See two
|
|
72
|
+
examples of this below.
|
|
73
|
+
|
|
74
|
+
Also, add whatever file the remote_bundle_cache_adapter module is defined in to `config.dependency_globs`.
|
|
75
|
+
|
|
76
|
+
If there are any other files for which changes should bust the fragment cache for
|
|
77
|
+
cached_react_component and cached_react_component_hash, add those as well to `config.dependency_globs`. This should include any files used to generate the JSON props, webpack and/or Shakapacker configuration files, and package lockfiles.
|
|
78
|
+
|
|
79
|
+
To simplify your configuration, entire directories can be added to `config.dependency_globs` & then any irrelevant files or subdirectories can be added to `config.excluded_dependency_globs`
|
|
80
|
+
|
|
81
|
+
For example:
|
|
82
|
+
```ruby
|
|
83
|
+
config.dependency_globs = [ File.join(Rails.root, "app", "views", "**", "*.jbuilder") ]
|
|
84
|
+
config.excluded_dependency_globs = [ File.join(Rails.root, "app", "views", "**", "dont_hash_this.jbuilder") ]
|
|
85
|
+
```
|
|
86
|
+
will hash all files in `app/views` that have the `jbuilder` extension except for any file named `dont_hash_this.jbuilder`.
|
|
87
|
+
|
|
88
|
+
The goal is that Ruby only changes that don't affect your webpack bundles don't change the cache keys, and anything that could affect the bundles MUST change the cache keys!
|
|
89
|
+
|
|
90
|
+
### 3. Remove any call to rake task `react_on_rails_pro:pre_stage_bundle_for_node_renderer`
|
|
91
|
+
This task is called automaticaly if you're using bundle caching.
|
|
92
|
+
```ruby
|
|
93
|
+
Rake::Task['react_on_rails_pro:pre_stage_bundle_for_node_renderer'].invoke
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### Custom ENV cache keys
|
|
97
|
+
Check your webpack config for the webpack.DefinePlugin. That allows JS code to use
|
|
98
|
+
`process.env.MY_ENV_VAR` resulting in bundles that differ depending on the ENV value set.
|
|
99
|
+
|
|
100
|
+
Thus, if you access these `process.env.MY_ENV_VAR` in your JS code, then you need to include such
|
|
101
|
+
ENV vars in return value of the `cache keys` method.
|
|
102
|
+
|
|
103
|
+
A much better approach than accessing `process.env` is to use the
|
|
104
|
+
`config/initializers/react_on_rails.rb` setting for the`config.rendering_extension` to always
|
|
105
|
+
pass some values into the rendering props.
|
|
106
|
+
|
|
107
|
+
See [our railsContext docs](https://www.shakacode.com/react-on-rails/docs/basics/render-functions-and-railscontext/#customization-of-the-railscontext) for more details.
|
|
108
|
+
|
|
109
|
+
Also, if your webpack build process depends on any ENV values, then you will also need to add those
|
|
110
|
+
to return value of the `cache_keys` method.
|
|
111
|
+
|
|
112
|
+
Note, the NODE_ENV value is always included in the cache_keys.
|
|
113
|
+
|
|
114
|
+
Another use of the ENV values would be a cache version, so incrementing this ENV value
|
|
115
|
+
would force a new cache value.
|
|
116
|
+
|
|
117
|
+
## Disabling via an ENV value
|
|
118
|
+
Once configured for bundle caching, ReactOnRailsPro::AssetsPrecompile's caching functionality
|
|
119
|
+
can be disabled by setting ENV["DISABLE_PRECOMPILE_CACHE"] equal to "true"
|
|
120
|
+
|
|
121
|
+
### Examples of `remote_bundle_cache_adapter`:
|
|
122
|
+
|
|
123
|
+
#### S3BundleCacheAdapter
|
|
124
|
+
Example of a module for custom methods for the `remote_bundle_cache_adapter`.
|
|
125
|
+
|
|
126
|
+
Note, S3UploadService is your own code that fetches and uploads.
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
class S3BundleCacheAdapter
|
|
130
|
+
# Optional
|
|
131
|
+
# return an Array of Strings that should get added to the cache key.
|
|
132
|
+
# These are values to put in the cache key based on either using the webpack.DefinePlugin
|
|
133
|
+
# or webpack compilation varying by the ENV values.
|
|
134
|
+
# See the use of the webpack.DefinePlugin. That allows JS code to use
|
|
135
|
+
# process.env.MY_ENV_VAR resulting in bundles that differ depending on the ENV value set
|
|
136
|
+
# when building the bundles.
|
|
137
|
+
# Note, NODE_ENV is automatically included in the default cache key.
|
|
138
|
+
# Also, we can have an ENV value be a cache version, so incrementing this ENV value
|
|
139
|
+
# would force a new cache value.
|
|
140
|
+
def self.cache_keys
|
|
141
|
+
[Rails.env, ENV['SOME_ENV_VALUE']]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# return value is unused
|
|
145
|
+
# This command should build the bundles
|
|
146
|
+
def self.build
|
|
147
|
+
Rake.sh(ReactOnRails::Utils.prepend_cd_node_modules_directory('yarn start build.prod').to_s)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# parameter zipped_bundles_filename will be a string
|
|
151
|
+
# should return the zipped file as a string if successful & nil if not
|
|
152
|
+
def self.fetch(zipped_bundles_filename:)
|
|
153
|
+
result = S3UploadService.new.fetch_object(zipped_bundles_filename)
|
|
154
|
+
result.get.body.read if result
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Optional: method to return an array of extra files paths, that require caching.
|
|
158
|
+
# These files get placed at the `extra_files` directory at the top of the zipfile
|
|
159
|
+
# and are moved to the original places after unzipping the bundles.
|
|
160
|
+
def self.extra_files_to_cache
|
|
161
|
+
[ Rails.root.join("app", "javascript", "utils", "operationStore.json") ]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# parameter zipped_bundles_filepath will be a Pathname
|
|
165
|
+
# return value is unused
|
|
166
|
+
def self.upload(zipped_bundles_filepath:)
|
|
167
|
+
return unless ENV['UPLOAD_BUNDLES_TO_S3'] == 'true'
|
|
168
|
+
|
|
169
|
+
zipped_bundles_filename = zipped_bundles_filepath.basename.to_s
|
|
170
|
+
puts "Bundles are being uploaded to s3 as #{zipped_bundles_filename}"
|
|
171
|
+
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
172
|
+
S3UploadService.new.upload_object(zipped_bundles_filename,
|
|
173
|
+
File.read(zipped_bundles_filepath, mode: 'rb'),
|
|
174
|
+
'application/zip', expiration_months: 12)
|
|
175
|
+
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
176
|
+
elapsed = (ending - starting).round(2)
|
|
177
|
+
puts "Bundles uploaded to s3 as #{zipped_bundles_filename} in #{elapsed} seconds"
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
#### LocalBundleCacheAdapter
|
|
183
|
+
Example of a module for custom methods for the `remote_bundle_cache_adapter` that does not save files
|
|
184
|
+
remotely. Only local files are used.
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
class LocalBundleCacheAdapter
|
|
188
|
+
def self.cache_keys
|
|
189
|
+
# if no additional cache keys, return an empty array
|
|
190
|
+
[]
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def self.build
|
|
194
|
+
Rake.sh(ReactOnRails::Utils.prepend_cd_node_modules_directory('yarn start build.prod').to_s)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def self.fetch(zipped_bundles_filename:)
|
|
198
|
+
# no-op
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def self.upload(zipped_bundles_filepath:)
|
|
202
|
+
# no-op
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
```
|