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,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,4 @@
1
+ module.exports = {
2
+ presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'],
3
+ plugins: ['@babel/plugin-syntax-import-attributes'],
4
+ };
@@ -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
+ ```