react_on_rails_pro 16.2.0.beta.12 → 16.2.0.beta.16

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.
@@ -128,7 +128,11 @@ module ReactOnRailsProHelper
128
128
  # Because setting prerender to false is equivalent to calling react_component with prerender: false
129
129
  options[:prerender] = true
130
130
  options = options.merge(immediate_hydration: true) unless options.key?(:immediate_hydration)
131
- run_stream_inside_fiber do
131
+
132
+ # Extract streaming-specific callback
133
+ on_complete = options.delete(:on_complete)
134
+
135
+ consumer_stream_async(on_complete: on_complete) do
132
136
  internal_stream_react_component(component_name, options)
133
137
  end
134
138
  end
@@ -185,7 +189,11 @@ module ReactOnRailsProHelper
185
189
  # rsc_payload_react_component doesn't have the prerender option
186
190
  # Because setting prerender to false will not do anything
187
191
  options[:prerender] = true
188
- run_stream_inside_fiber do
192
+
193
+ # Extract streaming-specific callback
194
+ on_complete = options.delete(:on_complete)
195
+
196
+ consumer_stream_async(on_complete: on_complete) do
189
197
  internal_rsc_payload_react_component(component_name, options)
190
198
  end
191
199
  end
@@ -209,6 +217,63 @@ module ReactOnRailsProHelper
209
217
  end
210
218
  end
211
219
 
220
+ # Renders a React component asynchronously, returning an AsyncValue immediately.
221
+ # Multiple async_react_component calls will execute their HTTP rendering requests
222
+ # concurrently instead of sequentially.
223
+ #
224
+ # Requires the controller to include ReactOnRailsPro::AsyncRendering and call
225
+ # enable_async_react_rendering.
226
+ #
227
+ # @param component_name [String] Name of your registered component
228
+ # @param options [Hash] Same options as react_component
229
+ # @return [ReactOnRailsPro::AsyncValue] Call .value to get the rendered HTML
230
+ #
231
+ # @example
232
+ # <% header = async_react_component("Header", props: @header_props) %>
233
+ # <% sidebar = async_react_component("Sidebar", props: @sidebar_props) %>
234
+ # <%= header.value %>
235
+ # <%= sidebar.value %>
236
+ #
237
+ def async_react_component(component_name, options = {})
238
+ unless defined?(@react_on_rails_async_barrier) && @react_on_rails_async_barrier
239
+ raise ReactOnRailsPro::Error,
240
+ "async_react_component requires AsyncRendering concern. " \
241
+ "Include ReactOnRailsPro::AsyncRendering in your controller and call enable_async_react_rendering."
242
+ end
243
+
244
+ task = @react_on_rails_async_barrier.async do
245
+ react_component(component_name, options)
246
+ end
247
+
248
+ ReactOnRailsPro::AsyncValue.new(task: task)
249
+ end
250
+
251
+ # Renders a React component asynchronously with caching support.
252
+ # Cache lookup is synchronous - cache hits return immediately without async.
253
+ # Cache misses trigger async render and cache the result on completion.
254
+ #
255
+ # All the same options as cached_react_component apply:
256
+ # 1. You must pass the props as a block (evaluated only on cache miss)
257
+ # 2. Provide the cache_key option
258
+ # 3. Optionally provide :cache_options for Rails.cache (expires_in, etc.)
259
+ # 4. Provide :if or :unless for conditional caching
260
+ #
261
+ # @param component_name [String] Name of your registered component
262
+ # @param options [Hash] Options including cache_key and cache_options
263
+ # @yield Block that returns props (evaluated only on cache miss)
264
+ # @return [ReactOnRailsPro::AsyncValue, ReactOnRailsPro::ImmediateAsyncValue]
265
+ #
266
+ # @example
267
+ # <% card = cached_async_react_component("ProductCard", cache_key: @product) { @product.to_props } %>
268
+ # <%= card.value %>
269
+ #
270
+ def cached_async_react_component(component_name, raw_options = {}, &block)
271
+ ReactOnRailsPro::Utils.with_trace(component_name) do
272
+ check_caching_options!(raw_options, block)
273
+ fetch_async_react_component(component_name, raw_options, &block)
274
+ end
275
+ end
276
+
212
277
  if defined?(ScoutApm)
213
278
  include ScoutApm::Tracer
214
279
  instrument_method :cached_react_component, type: "ReactOnRails", name: "cached_react_component"
@@ -246,30 +311,29 @@ module ReactOnRailsProHelper
246
311
  load_pack_for_generated_component(component_name, render_options)
247
312
 
248
313
  initial_result, *rest_chunks = cached_chunks
249
- hit_fiber = Fiber.new do
250
- rest_chunks.each { |chunk| Fiber.yield(chunk) }
251
- nil
314
+
315
+ # Enqueue remaining chunks asynchronously
316
+ @async_barrier.async do
317
+ rest_chunks.each { |chunk| @main_output_queue.enqueue(chunk) }
252
318
  end
253
- @rorp_rendering_fibers << hit_fiber
319
+
320
+ # Return first chunk directly
254
321
  initial_result
255
322
  end
256
323
 
257
324
  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
325
+ cache_aware_options = raw_options.merge(
326
+ on_complete: lambda { |chunks|
327
+ Rails.cache.write(view_cache_key, chunks, raw_options[:cache_options] || {})
328
+ }
329
+ )
330
+
331
+ render_stream_component_with_props(
332
+ component_name,
333
+ cache_aware_options,
334
+ auto_load_bundle,
335
+ &block
336
+ )
273
337
  end
274
338
 
275
339
  def render_stream_component_with_props(component_name, raw_options, auto_load_bundle)
@@ -291,25 +355,117 @@ module ReactOnRailsProHelper
291
355
  raise ReactOnRailsPro::Error, "Option 'cache_key' is required for React on Rails caching"
292
356
  end
293
357
 
294
- def run_stream_inside_fiber
295
- if @rorp_rendering_fibers.nil?
358
+ # Async version of fetch_react_component. Handles cache lookup synchronously,
359
+ # returns ImmediateAsyncValue on hit, AsyncValue on miss.
360
+ def fetch_async_react_component(component_name, raw_options, &block)
361
+ unless defined?(@react_on_rails_async_barrier) && @react_on_rails_async_barrier
362
+ raise ReactOnRailsPro::Error,
363
+ "cached_async_react_component requires AsyncRendering concern. " \
364
+ "Include ReactOnRailsPro::AsyncRendering in your controller and call enable_async_react_rendering."
365
+ end
366
+
367
+ # Check conditional caching (:if / :unless options)
368
+ unless ReactOnRailsPro::Cache.use_cache?(raw_options)
369
+ return render_async_react_component_uncached(component_name, raw_options, &block)
370
+ end
371
+
372
+ cache_key = ReactOnRailsPro::Cache.react_component_cache_key(component_name, raw_options)
373
+ cache_options = raw_options[:cache_options] || {}
374
+ Rails.logger.debug { "React on Rails Pro async cache_key is #{cache_key.inspect}" }
375
+
376
+ # Synchronous cache lookup
377
+ cached_result = Rails.cache.read(cache_key, cache_options)
378
+ if cached_result
379
+ Rails.logger.debug { "React on Rails Pro async cache HIT for #{cache_key.inspect}" }
380
+ render_options = ReactOnRails::ReactComponent::RenderOptions.new(
381
+ react_component_name: component_name,
382
+ options: raw_options
383
+ )
384
+ load_pack_for_generated_component(component_name, render_options)
385
+ return ReactOnRailsPro::ImmediateAsyncValue.new(cached_result)
386
+ end
387
+
388
+ Rails.logger.debug { "React on Rails Pro async cache MISS for #{cache_key.inspect}" }
389
+ render_async_react_component_with_cache(component_name, raw_options, cache_key, cache_options, &block)
390
+ end
391
+
392
+ # Renders async without caching (when :if/:unless conditions disable cache)
393
+ def render_async_react_component_uncached(component_name, raw_options, &block)
394
+ options = prepare_async_render_options(raw_options, &block)
395
+
396
+ task = @react_on_rails_async_barrier.async do
397
+ react_component(component_name, options)
398
+ end
399
+
400
+ ReactOnRailsPro::AsyncValue.new(task: task)
401
+ end
402
+
403
+ # Renders async and writes to cache on completion
404
+ def render_async_react_component_with_cache(component_name, raw_options, cache_key, cache_options, &block)
405
+ options = prepare_async_render_options(raw_options, &block)
406
+
407
+ task = @react_on_rails_async_barrier.async do
408
+ result = react_component(component_name, options)
409
+ Rails.cache.write(cache_key, result, cache_options)
410
+ result
411
+ end
412
+
413
+ ReactOnRailsPro::AsyncValue.new(task: task)
414
+ end
415
+
416
+ def prepare_async_render_options(raw_options)
417
+ raw_options.merge(
418
+ props: yield,
419
+ skip_prerender_cache: true,
420
+ auto_load_bundle: ReactOnRails.configuration.auto_load_bundle || raw_options[:auto_load_bundle]
421
+ )
422
+ end
423
+
424
+ def consumer_stream_async(on_complete:)
425
+ require "async/variable"
426
+
427
+ if @async_barrier.nil?
296
428
  raise ReactOnRails::Error,
297
429
  "You must call stream_view_containing_react_components to render the view containing the react component"
298
430
  end
299
431
 
300
- rendering_fiber = Fiber.new do
432
+ # Create a variable to hold the first chunk for synchronous return
433
+ first_chunk_var = Async::Variable.new
434
+ all_chunks = [] if on_complete # Only collect if callback provided
435
+
436
+ # Start an async task on the barrier to stream all chunks
437
+ @async_barrier.async do
301
438
  stream = yield
302
- stream.each_chunk do |chunk|
303
- Fiber.yield chunk
304
- end
439
+ process_stream_chunks(stream, first_chunk_var, all_chunks)
440
+ on_complete&.call(all_chunks)
305
441
  end
306
442
 
307
- @rorp_rendering_fibers << rendering_fiber
443
+ # Wait for and return the first chunk (blocking)
444
+ first_chunk_var.wait
445
+ first_chunk_var.value
446
+ end
447
+
448
+ def process_stream_chunks(stream, first_chunk_var, all_chunks)
449
+ is_first = true
450
+
451
+ stream.each_chunk do |chunk|
452
+ # Check if client disconnected before processing chunk
453
+ break if response.stream.closed?
454
+
455
+ all_chunks&.push(chunk)
456
+
457
+ if is_first
458
+ # Store first chunk in variable for synchronous return
459
+ first_chunk_var.value = chunk
460
+ is_first = false
461
+ else
462
+ # Enqueue remaining chunks to main output queue
463
+ @main_output_queue.enqueue(chunk)
464
+ end
465
+ end
308
466
 
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
467
+ # Handle case where stream has no chunks
468
+ first_chunk_var.value = nil if is_first
313
469
  end
314
470
 
315
471
  def internal_stream_react_component(component_name, options = {})
@@ -1,12 +1,14 @@
1
1
  # Server-side rendering with code-splitting using Loadable/Components
2
+
2
3
  by ShakaCode
3
4
 
4
- *Last updated September 19, 2022*
5
+ _Last updated September 19, 2022_
5
6
 
6
7
  ## Introduction
8
+
7
9
  The [React library recommends](https://loadable-components.com/docs/getting-started/) the use of React.lazy for code splitting with dynamic imports except
8
10
  when using server-side rendering. In that case, as of February 2020, they recommend [Loadable Components](https://loadable-components.com)
9
- for server-side rendering with dynamic imports.
11
+ for server-side rendering with dynamic imports.
10
12
 
11
13
  Note, in 2019 and prior, the code-splitting feature was implemented using `react-loadable`. The React
12
14
  team no longer recommends that library. The new way is far preferable.
@@ -18,7 +20,8 @@ yarn add @loadable/babel-plugin @loadable/component @loadable/server @loadable/
18
20
  ```
19
21
 
20
22
  ### Summary
21
- - [`@loadable/babel-plugin`](https://loadable-components.com/docs/getting-started/) - The plugin transforms your code to be ready for Server Side Rendering.
23
+
24
+ - [`@loadable/babel-plugin`](https://loadable-components.com/docs/getting-started/) - The plugin transforms your code to be ready for Server Side Rendering.
22
25
  - `@loadable/component` - Main library for creating loadable components.
23
26
  - `@loadable/server` - Has functions for collecting chunks and provide style, script, link tags for the server.
24
27
  - `@loadable/webpack-plugin` - The plugin to create a stats file with all chunks, assets information.
@@ -35,15 +38,16 @@ See example of server configuration differences in the loadable-components [exam
35
38
  for server-side rendering](https://github.com/gregberge/loadable-components/blob/master/examples/server-side-rendering/webpack.config.babel.js)
36
39
 
37
40
  You need to configure 3 things:
38
- 1. `target`
39
- a. client-side: `web`
40
- b. server-side: `node`
41
+
42
+ 1. `target`
43
+ a. client-side: `web`
44
+ b. server-side: `node`
41
45
  2. `output.libraryTarget`
42
- a. client-side: `undefined`
43
- b. server-side: `commonjs2`
44
- 3. babel-loader options.caller = 'node' or 'web'
45
- 3. `plugins`
46
- a. server-side: `new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })`
46
+ a. client-side: `undefined`
47
+ b. server-side: `commonjs2`
48
+ 3. babel-loader options.caller = 'node' or 'web'
49
+ 4. `plugins`
50
+ a. server-side: `new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })`
47
51
 
48
52
  ```js
49
53
  {
@@ -58,14 +62,15 @@ You need to configure 3 things:
58
62
  Explanation:
59
63
 
60
64
  - `target: 'node'` is required to be able to run the server bundle with the dynamic import logic on nodejs.
61
- If that is not done, webpack will add and invoke browser-specific functions to fetch the chunks into the bundle, which throws an error on server-rendering.
65
+ If that is not done, webpack will add and invoke browser-specific functions to fetch the chunks into the bundle, which throws an error on server-rendering.
62
66
 
63
67
  - `new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })`
64
- The react_on_rails_pro node-renderer expects only one single server-bundle. In other words, we cannot and do not want to split the server bundle.
68
+ The react_on_rails_pro node-renderer expects only one single server-bundle. In other words, we cannot and do not want to split the server bundle.
65
69
 
66
70
  #### Client config
67
71
 
68
72
  For the client config we only need to add the plugin:
73
+
69
74
  ```js
70
75
  {
71
76
  plugins: [
@@ -74,30 +79,33 @@ For the client config we only need to add the plugin:
74
79
  ]
75
80
  }
76
81
  ```
82
+
77
83
  This plugin collects all the information about entrypoints, chunks, and files, that have these chunks and creates a stats file during client bundle build.
78
84
  This stats file is used later to map rendered components to file assets. While you can use any filename, our documentation will use the default name.
79
85
 
80
86
  ### Babel
81
87
 
82
88
  Per [the docs](https://loadable-components.com/docs/babel-plugin/#transformation):
89
+
83
90
  > The plugin transforms your code to be ready for Server Side Rendering
84
91
 
85
92
  Add this to `babel.config.js`:
93
+
86
94
  ```js
87
95
  {
88
96
  "plugins": ["@loadable/babel-plugin"]
89
97
  }
90
98
  ```
91
- https://loadable-components.com/docs/babel-plugin/
92
99
 
100
+ https://loadable-components.com/docs/babel-plugin/
93
101
 
94
102
  ### Convert components into loadable components
95
103
 
96
104
  Instead of importing the component directly, use a dynamic import:
97
105
 
98
106
  ```js
99
- import load from '@loadable/component'
100
- const MyComponent = load(() => import('./MyComponent'))
107
+ import load from '@loadable/component';
108
+ const MyComponent = load(() => import('./MyComponent'));
101
109
  ```
102
110
 
103
111
  ### Resolving issue with ChunkLoadError
@@ -118,22 +126,25 @@ const consoleDebug = (fn) => {
118
126
  console.debug(fn());
119
127
  }
120
128
  };
121
- const retry = (fn, retryMessage = '', retriesLeft = 3, interval = 500) => new Promise((resolve, reject) => {
122
- fn()
123
- .then(resolve)
124
- .catch(() => {
125
- setTimeout(() => {
126
- if (retriesLeft === 1) {
127
- console.warn(`Maximum retries exceeded, retryMessage: ${retryMessage}. Reloading page...`);
128
- window.location.reload();
129
- return;
130
- }
131
- // Passing on "reject" is the important part
132
- consoleDebug(() => `Trying request, retryMessage: ${retryMessage}, retriesLeft: ${retriesLeft - 1}`);
133
- retry(fn, retryMessage, retriesLeft - 1, interval).then(resolve, reject);
134
- }, interval);
135
- });
136
- });
129
+ const retry = (fn, retryMessage = '', retriesLeft = 3, interval = 500) =>
130
+ new Promise((resolve, reject) => {
131
+ fn()
132
+ .then(resolve)
133
+ .catch(() => {
134
+ setTimeout(() => {
135
+ if (retriesLeft === 1) {
136
+ console.warn(`Maximum retries exceeded, retryMessage: ${retryMessage}. Reloading page...`);
137
+ window.location.reload();
138
+ return;
139
+ }
140
+ // Passing on "reject" is the important part
141
+ consoleDebug(
142
+ () => `Trying request, retryMessage: ${retryMessage}, retriesLeft: ${retriesLeft - 1}`,
143
+ );
144
+ retry(fn, retryMessage, retriesLeft - 1, interval).then(resolve, reject);
145
+ }, interval);
146
+ });
147
+ });
137
148
  export default retry;
138
149
  ```
139
150
 
@@ -152,21 +163,21 @@ const HomePage = loadable(() => retry(() => import('./HomePage')));
152
163
 
153
164
  In the client bundle, we need to wrap the `hydrateRoot` call into a `loadableReady` function.
154
165
  So, hydration will be fired only after all necessary chunks preloads. In this example below,
155
- `ClientApp` is registering as `App`.
166
+ `ClientApp` is registering as `App`.
156
167
 
157
168
  ```js
158
169
  import React from 'react';
159
170
  import ReactOnRails from 'react-on-rails';
160
- import { hydrateRoot } from 'react-dom/client'
161
- import { loadableReady } from '@loadable/component'
171
+ import { hydrateRoot } from 'react-dom/client';
172
+ import { loadableReady } from '@loadable/component';
162
173
  import App from './App';
163
174
 
164
175
  const ClientApp = (props, railsContext, domId) => {
165
176
  loadableReady(() => {
166
- const root = document.getElementById(domId)
177
+ const root = document.getElementById(domId);
167
178
  hydrateRoot(root, <App {...props} />);
168
- })
169
- }
179
+ });
180
+ };
170
181
 
171
182
  ReactOnRails.register({
172
183
  App: ClientApp,
@@ -175,20 +186,20 @@ ReactOnRails.register({
175
186
 
176
187
  #### Server
177
188
 
178
- The purpose of the server function is to collect all rendered chunks and pass them as script, link,
179
- style tags to the Rails view. In this example below, `ServerApp` is registering as `App`.
189
+ The purpose of the server function is to collect all rendered chunks and pass them as script, link,
190
+ style tags to the Rails view. In this example below, `ServerApp` is registering as `App`.
180
191
 
181
192
  ```js
182
193
  import React from 'react';
183
194
  import ReactOnRails from 'react-on-rails';
184
- import { ChunkExtractor } from '@loadable/server'
185
- import App from './App'
186
- import path from 'path'
195
+ import { ChunkExtractor } from '@loadable/server';
196
+ import App from './App';
197
+ import path from 'path';
187
198
 
188
199
  const ServerApp = (props, railsContext) => {
189
200
  // This loadable-stats file was generated by `LoadablePlugin` in client webpack config.
190
201
  // You must configure the path to resolve per your setup. If you are copying the file to
191
- // a remote server, the file should be a sibling of this file.
202
+ // a remote server, the file should be a sibling of this file.
192
203
  // __dirname is going to be the directory where the server-bundle.js exists
193
204
  // Note, React on Rails Pro automatically copies the loadable-stats.json to the same place as the
194
205
  // server-bundle.js. Thus, the __dirname of this code is where we can find loadable-stats.json.
@@ -198,10 +209,10 @@ const ServerApp = (props, railsContext) => {
198
209
  // This object is used to search filenames by corresponding chunk names.
199
210
  // See https://loadable-components.com/docs/api-loadable-server/#chunkextractor
200
211
  // for the entryPoints, pass an array of all your entryPoints using dynamic imports
201
- const extractor = new ChunkExtractor({ statsFile, entrypoints: ['client-bundle'] })
212
+ const extractor = new ChunkExtractor({ statsFile, entrypoints: ['client-bundle'] });
202
213
 
203
214
  // It creates the wrapper `ChunkExtractorManager` around `App` to collect chunk names of rendered components.
204
- const jsx = extractor.collectChunks(<App {...props} railsContext={railsContext} />)
215
+ const jsx = extractor.collectChunks(<App {...props} railsContext={railsContext} />);
205
216
 
206
217
  const componentHtml = renderToString(jsx);
207
218
 
@@ -211,8 +222,8 @@ const ServerApp = (props, railsContext) => {
211
222
  // Returns all the files with rendered chunks for furture insert into rails view.
212
223
  linkTags: extractor.getLinkTags(),
213
224
  styleTags: extractor.getStyleTags(),
214
- scriptTags: extractor.getScriptTags()
215
- }
225
+ scriptTags: extractor.getScriptTags(),
226
+ },
216
227
  };
217
228
  };
218
229
 
@@ -224,6 +235,7 @@ ReactOnRails.register({
224
235
  ## Configure react_on_rails_pro
225
236
 
226
237
  ### React on Rails Pro
238
+
227
239
  You must set `config.assets_top_copy` so that the node-renderer will have access to the loadable-stats.json.
228
240
 
229
241
  ```ruby
@@ -233,15 +245,16 @@ You must set `config.assets_top_copy` so that the node-renderer will have access
233
245
  Your server rendering code, per the above, will find this file like this:
234
246
 
235
247
  ```js
236
- const statsFile = path.resolve(__dirname, 'loadable-stats.json');
237
- ```
248
+ const statsFile = path.resolve(__dirname, 'loadable-stats.json');
249
+ ```
238
250
 
239
251
  Note, if `__dirname` is not working in your webpack build, that's because you didn't set `node: false`
240
252
  in your webpack configuration. That turns off the polyfills for things like `__dirname`.
241
253
 
242
-
243
254
  ### Node Renderer
255
+
244
256
  In your `node-renderer.js` file which runs node renderer, you need to specify `supportModules` options as follows:
257
+
245
258
  ```js
246
259
  const path = require('path');
247
260
  const env = process.env;
@@ -261,7 +274,7 @@ reactOnRailsProNodeRenderer(config);
261
274
  ```erb
262
275
  <% res = react_component_hash("App", props: {}, prerender: true) %>
263
276
  <%= content_for :link_tags, res['linkTags'] %>
264
- <%= content_for :style_tags, res['styleTags'] %>
277
+ <%= content_for :style_tags, res['styleTags'] %>
265
278
 
266
279
  <%= res['componentHtml'].html_safe %>
267
280
 
@@ -269,6 +282,7 @@ reactOnRailsProNodeRenderer(config);
269
282
  ```
270
283
 
271
284
  ## Making HMR Work
285
+
272
286
  To make HMR work, it's best to disable loadable-components when using the Dev Server.
273
287
  Note: you will need access to our **private** React on Rails Pro repository to open the following links.
274
288
 
@@ -277,9 +291,11 @@ Take a look at the code searches for ['imports-loadable'](https://github.com/sha
277
291
  The general concept is that we have a non-loadable, HMR-ready, file that substitutes for the loadable-enabled one, with the suffixes `imports-hmr.js` instead of `imports-loadable.js`
278
292
 
279
293
  ### Webpack configuration
294
+
280
295
  Use the [NormalModuleReplacement plugin](https://webpack.js.org/plugins/normal-module-replacement-plugin/):
281
296
 
282
297
  [code](https://github.com/shakacode/react_on_rails_pro/blob/a361f4e163b9170f180ae07ee312fb9b4c719fc3/spec/dummy/config/webpack/environment.js#L81-L91)
298
+
283
299
  ```js
284
300
  if (isWebpackDevServer) {
285
301
  environment.plugins.append(
@@ -305,7 +321,7 @@ Note: you will need access to our **private** React on Rails Pro repository to o
305
321
  ### Client-Side Startup
306
322
 
307
323
  - [spec/dummy/client/app/loadable/loadable-client.imports-hmr.js](https://github.com/shakacode/react_on_rails_pro/blob/master/spec/dummy/client/app/loadable/loadable-client.imports-hmr.js)
308
- - [spec/dummy/client/app/loadable/loadable-client.imports-loadable.js](https://github.com/shakacode/react_on_rails_pro/blob/master/spec/dummy/client/app/loadable/loadable-client.imports-loadable.js)
324
+ - [spec/dummy/client/app/loadable/loadable-client.imports-loadable.jsx](https://github.com/shakacode/react_on_rails_pro/blob/master/spec/dummy/client/app/loadable/loadable-client.imports-loadable.jsx)
309
325
 
310
326
  ### Server-Side Startup
311
327
 
data/eslint.config.mjs CHANGED
@@ -3,7 +3,6 @@ import { includeIgnoreFile } from '@eslint/compat';
3
3
  import js from '@eslint/js';
4
4
  import { FlatCompat } from '@eslint/eslintrc';
5
5
  import { defineConfig, globalIgnores } from 'eslint/config';
6
- import importPlugin from 'eslint-plugin-import';
7
6
  import jest from 'eslint-plugin-jest';
8
7
  import prettierRecommended from 'eslint-plugin-prettier/recommended';
9
8
  import globals from 'globals';
@@ -22,8 +21,9 @@ export default defineConfig([
22
21
  'spec/react_on_rails/dummy-for-generators',
23
22
  // includes some generated code
24
23
  'spec/dummy/client/app/packs/server-bundle.js',
25
- 'packages/node-renderer/lib/',
26
- 'packages/node-renderer/tests/fixtures',
24
+ '../packages/react-on-rails/', // Ignore open-source package (has its own linting)
25
+ '../packages/react-on-rails-pro-node-renderer/lib/',
26
+ '../packages/react-on-rails-pro-node-renderer/tests/fixtures',
27
27
  '**/node_modules/',
28
28
  '**/assets/webpack/',
29
29
  '**/generated/',
@@ -129,7 +129,7 @@ export default defineConfig([
129
129
  },
130
130
  {
131
131
  files: ['**/*.ts{x,}'],
132
- extends: [importPlugin.flatConfigs.typescript, typescriptEslint.configs.strictTypeChecked],
132
+ extends: [typescriptEslint.configs.strictTypeChecked],
133
133
 
134
134
  languageOptions: {
135
135
  parserOptions: {
@@ -166,7 +166,7 @@ export default defineConfig([
166
166
  },
167
167
  },
168
168
  {
169
- files: ['packages/node-renderer/tests/**', '**/*.test.{js,jsx,ts,tsx}'],
169
+ files: ['../packages/react-on-rails-pro-node-renderer/tests/**', '**/*.test.{js,jsx,ts,tsx}'],
170
170
 
171
171
  extends: [jest.configs['flat/recommended'], jest.configs['flat/style']],
172
172
 
@@ -183,8 +183,8 @@ export default defineConfig([
183
183
  },
184
184
  },
185
185
  {
186
- files: ['packages/node-renderer/src/integrations/**'],
187
- ignores: ['packages/node-renderer/src/integrations/api.ts'],
186
+ files: ['../packages/react-on-rails-pro-node-renderer/src/integrations/**'],
187
+ ignores: ['../packages/react-on-rails-pro-node-renderer/src/integrations/api.ts'],
188
188
 
189
189
  rules: {
190
190
  // Integrations should only use the public integration API
@@ -214,6 +214,25 @@ export default defineConfig([
214
214
  'react-hooks/rules-of-hooks': ['off'],
215
215
  },
216
216
  },
217
+ {
218
+ // Dummy apps have dependencies managed separately and may not be installed
219
+ files: ['spec/dummy/**/*', 'spec/execjs-compatible-dummy/**/*'],
220
+ rules: {
221
+ 'import/no-unresolved': 'off',
222
+ 'import/named': 'off',
223
+ },
224
+ },
225
+ {
226
+ // Pro packages use monorepo workspace imports that ESLint can't resolve
227
+ // TypeScript compiler validates these imports
228
+ files: ['../packages/react-on-rails-pro/**/*', '../packages/react-on-rails-pro-node-renderer/**/*'],
229
+ rules: {
230
+ 'import/named': 'off',
231
+ 'import/no-unresolved': 'off',
232
+ 'import/no-cycle': 'off',
233
+ 'import/extensions': 'off',
234
+ },
235
+ },
217
236
  // must be the last config in the array
218
237
  // https://github.com/prettier/eslint-plugin-prettier?tab=readme-ov-file#configuration-new-eslintconfigjs
219
238
  prettierRecommended,
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReactOnRailsPro
4
+ # AsyncValue wraps an Async task to provide a simple interface for
5
+ # retrieving the result of an async react_component render.
6
+ #
7
+ # @example
8
+ # async_value = async_react_component("MyComponent", props: { name: "World" })
9
+ # # ... do other work ...
10
+ # html = async_value.value # blocks until result is ready
11
+ #
12
+ class AsyncValue
13
+ def initialize(task:)
14
+ @task = task
15
+ end
16
+
17
+ # Blocks until result is ready, returns HTML string.
18
+ # If the async task raised an exception, it will be re-raised here.
19
+ def value
20
+ @task.wait
21
+ end
22
+
23
+ def resolved?
24
+ @task.finished?
25
+ end
26
+
27
+ def to_s
28
+ value.to_s
29
+ end
30
+
31
+ def html_safe
32
+ value.html_safe
33
+ end
34
+ end
35
+ end