Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add support for RSC #1644

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
react_on_rails (14.0.5)
react_on_rails (15.0.0.alpha.1)
addressable
connection_pool
execjs (~> 2.5)
Expand Down
79 changes: 52 additions & 27 deletions lib/react_on_rails/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,29 +124,15 @@ def react_component(component_name, options = {})
# @option options [Boolean] :raise_on_prerender_error Set to true to raise exceptions during server-side rendering
# Any other options are passed to the content tag, including the id.
def stream_react_component(component_name, options = {})
unless ReactOnRails::Utils.react_on_rails_pro?
raise ReactOnRails::Error,
"You must use React on Rails Pro to use the stream_react_component method."
end

if @rorp_rendering_fibers.nil?
raise ReactOnRails::Error,
"You must call stream_view_containing_react_components to render the view containing the react component"
run_stream_inside_fiber do
internal_stream_react_component(component_name, options)
end
end

rendering_fiber = Fiber.new do
stream = internal_stream_react_component(component_name, options)
stream.each_chunk do |chunk|
Fiber.yield chunk
end
def rsc_react_component(component_name, options = {})
run_stream_inside_fiber do
internal_rsc_react_component(component_name, options)
end

@rorp_rendering_fibers << rendering_fiber

# return the first chunk of the fiber
# It contains the initial html of the component
# all updates will be appended to the stream sent to browser
rendering_fiber.resume
end

# react_component_hash is used to return multiple HTML strings for server rendering, such as for
Expand Down Expand Up @@ -375,19 +361,46 @@ def load_pack_for_generated_component(react_component_name, render_options)
return unless render_options.auto_load_bundle

ReactOnRails::PackerUtils.raise_nested_entries_disabled unless ReactOnRails::PackerUtils.nested_entries?
if Rails.env.development?
is_component_pack_present = File.exist?(generated_components_pack_path(react_component_name))
raise_missing_autoloaded_bundle(react_component_name) unless is_component_pack_present
end
append_javascript_pack_tag("generated/#{react_component_name}",
defer: ReactOnRails.configuration.defer_generated_component_packs)
append_stylesheet_pack_tag("generated/#{react_component_name}")
append_javascript_pack_tag("client-bundle")
# if Rails.env.development?
# is_component_pack_present = File.exist?(generated_components_pack_path(react_component_name))
# raise_missing_autoloaded_bundle(react_component_name) unless is_component_pack_present
# end
# append_javascript_pack_tag("generated/#{react_component_name}",
# defer: ReactOnRails.configuration.defer_generated_component_packs)
# append_stylesheet_pack_tag("generated/#{react_component_name}")
AbanoubGhadban marked this conversation as resolved.
Show resolved Hide resolved
end

# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity

private

def run_stream_inside_fiber
unless ReactOnRails::Utils.react_on_rails_pro?
raise ReactOnRails::Error,
"You must use React on Rails Pro to use the stream_react_component method."
end

if @rorp_rendering_fibers.nil?
raise ReactOnRails::Error,
"You must call stream_view_containing_react_components to render the view containing the react component"
end

rendering_fiber = Fiber.new do
stream = yield
stream.each_chunk do |chunk|
Fiber.yield chunk
end
end

@rorp_rendering_fibers << rendering_fiber

# return the first chunk of the fiber
# It contains the initial html of the component
# all updates will be appended to the stream sent to browser
rendering_fiber.resume
end

def internal_stream_react_component(component_name, options = {})
options = options.merge(stream?: true)
result = internal_react_component(component_name, options)
Expand All @@ -398,6 +411,15 @@ def internal_stream_react_component(component_name, options = {})
)
end

def internal_rsc_react_component(react_component_name, options = {})
options = options.merge(rsc?: true)
render_options = create_render_options(react_component_name, options)
json_stream = server_rendered_react_component(render_options)
json_stream.transform do |chunk|
chunk[:html].html_safe
end
end
Comment on lines +413 to +420
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance error handling and type safety for RSC

The RSC implementation should include:

  1. Type validation for the JSON stream
  2. Error handling for transformation failures
 def internal_rsc_react_component(react_component_name, options = {})
   options = options.merge(rsc?: true)
   render_options = create_render_options(react_component_name, options)
   json_stream = server_rendered_react_component(render_options)
+  raise ReactOnRails::Error, "Invalid RSC stream" unless json_stream.respond_to?(:transform)
   json_stream.transform do |chunk|
+    raise ReactOnRails::Error, "Invalid chunk format" unless chunk.is_a?(Hash) && chunk.key?(:html)
     chunk[:html].html_safe
   end
+rescue StandardError => e
+  raise ReactOnRails::PrerenderError.new(
+    component_name: react_component_name,
+    err: e,
+    props: options[:props]
+  )
 end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def internal_rsc_react_component(react_component_name, options = {})
options = options.merge(rsc?: true)
render_options = create_render_options(react_component_name, options)
json_stream = server_rendered_react_component(render_options)
json_stream.transform do |chunk|
chunk[:html].html_safe
end
end
def internal_rsc_react_component(react_component_name, options = {})
options = options.merge(rsc?: true)
render_options = create_render_options(react_component_name, options)
json_stream = server_rendered_react_component(render_options)
raise ReactOnRails::Error, "Invalid RSC stream" unless json_stream.respond_to?(:transform)
json_stream.transform do |chunk|
raise ReactOnRails::Error, "Invalid chunk format" unless chunk.is_a?(Hash) && chunk.key?(:html)
chunk[:html].html_safe
end
rescue StandardError => e
raise ReactOnRails::PrerenderError.new(
component_name: react_component_name,
err: e,
props: options[:props]
)
end


def generated_components_pack_path(component_name)
"#{ReactOnRails::PackerUtils.packer_source_entry_path}/generated/#{component_name}.js"
end
Expand Down Expand Up @@ -636,6 +658,9 @@ def server_rendered_react_component(render_options)
js_code: js_code)
end

# TODO: handle errors for rsc streams
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be fixed later on the PR that fixes
https://github.com/shakacode/react_on_rails_pro/issues/465

return result if render_options.rsc?

Comment on lines +660 to +662
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Implement error handling for RSC streams

The TODO comment indicates that error handling for RSC streams is not yet implemented. This is critical for production reliability as unhandled errors in server-side rendering could lead to silent failures or degraded user experience.

Consider implementing error handling similar to the existing streaming error handling logic above.

Would you like help implementing the error handling logic for RSC streams? I can provide a solution that follows the existing patterns in the codebase.

if render_options.stream?
result.transform do |chunk_json_result|
if should_raise_streaming_prerender_error?(chunk_json_result, render_options)
Expand Down
4 changes: 4 additions & 0 deletions lib/react_on_rails/react_component/render_options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ def stream?
options[:stream?]
end

def rsc?
options[:rsc?]
end

private

attr_reader :options
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def exec_server_render_js(js_code, render_options, js_evaluator = nil)
@file_index += 1
end
begin
result = if render_options.stream?
result = if render_options.stream? || render_options.rsc?
AbanoubGhadban marked this conversation as resolved.
Show resolved Hide resolved
js_evaluator.eval_streaming_js(js_code, render_options)
else
js_evaluator.eval_js(js_code, render_options)
Expand All @@ -76,7 +76,7 @@ def exec_server_render_js(js_code, render_options, js_evaluator = nil)
raise ReactOnRails::Error, msg, err.backtrace
end

return parse_result_and_replay_console_messages(result, render_options) unless render_options.stream?
return parse_result_and_replay_console_messages(result, render_options) unless render_options.stream? || render_options.rsc?

# Streamed component is returned as stream of strings.
# We need to parse each chunk and replay the console messages.
Expand Down Expand Up @@ -231,6 +231,7 @@ def parse_result_and_replay_console_messages(result_string, render_options)
begin
result = JSON.parse(result_string)
rescue JSON::ParserError => e
return { html: result_string }
AbanoubGhadban marked this conversation as resolved.
Show resolved Hide resolved
raise ReactOnRails::JsonParseError.new(parse_error: e, json: result_string)
AbanoubGhadban marked this conversation as resolved.
Show resolved Hide resolved
end

Expand Down
59 changes: 36 additions & 23 deletions lib/react_on_rails/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def self.server_bundle_path_is_http?
server_bundle_js_file_path =~ %r{https?://}
end

def self.server_bundle_js_file_path
def self.bundle_js_file_path(bundle_name)
AbanoubGhadban marked this conversation as resolved.
Show resolved Hide resolved
# Either:
# 1. Using same bundle for both server and client, so server bundle will be hashed in manifest
# 2. Using a different bundle (different Webpack config), so file is not hashed, and
Expand All @@ -76,36 +76,49 @@ def self.server_bundle_js_file_path
# a. The webpack manifest plugin would have a race condition where the same manifest.json
# is edited by both the webpack-dev-server
# b. There is no good reason to hash the server bundle name.
return @server_bundle_path if @server_bundle_path && !Rails.env.development?

bundle_name = ReactOnRails.configuration.server_bundle_js_file
@server_bundle_path = if ReactOnRails::PackerUtils.using_packer?
begin
bundle_js_file_path(bundle_name)
rescue Object.const_get(
ReactOnRails::PackerUtils.packer_type.capitalize
)::Manifest::MissingEntryError
File.expand_path(
File.join(ReactOnRails::PackerUtils.packer_public_output_path,
bundle_name)
)
end
else
bundle_js_file_path(bundle_name)
end
end

def self.bundle_js_file_path(bundle_name)
if ReactOnRails::PackerUtils.using_packer? && bundle_name != "manifest.json"
ReactOnRails::PackerUtils.bundle_js_uri_from_packer(bundle_name)
begin
ReactOnRails::PackerUtils.bundle_js_uri_from_packer(bundle_name)
rescue Object.const_get(
ReactOnRails::PackerUtils.packer_type.capitalize
)::Manifest::MissingEntryError
Comment on lines +82 to +84
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it could easily raise an error itself. Any way around it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added enough specs in utils_spec.rb to ensure that it doesn't cause errors if the packer is webpacker or shaka packer'. For now, React on Rails only supports these packers. If we plan to add support for more packers, we can refactor the PackerUtils` module in a follow-up PR.

Comment on lines +82 to +84
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error handling specificity

The current error handling using Object.const_get is overly broad and could mask other legitimate errors. Consider being more specific about the error type.

-        rescue Object.const_get(
-          ReactOnRails::PackerUtils.packer_type.capitalize
-        )::Manifest::MissingEntryError
+        rescue => e
+          packer_error = "#{ReactOnRails::PackerUtils.packer_type.capitalize}::Manifest::MissingEntryError"
+          raise e unless e.class.name == packer_error

Committable suggestion skipped: line range outside the PR's diff.

File.expand_path(
File.join(ReactOnRails::PackerUtils.packer_public_output_path,
bundle_name)
)
end
else
# Default to the non-hashed name in the specified output directory, which, for legacy
# React on Rails, this is the output directory picked up by the asset pipeline.
# For Shakapacker, this is the public output path defined in the (shaka/web)packer.yml file.
# For Webpacker, this is the public output path defined in the webpacker.yml file.
AbanoubGhadban marked this conversation as resolved.
Show resolved Hide resolved
File.join(generated_assets_full_path, bundle_name)
end
end

def self.server_bundle_js_file_path
AbanoubGhadban marked this conversation as resolved.
Show resolved Hide resolved
# Either:
# 1. Using same bundle for both server and client, so server bundle will be hashed in manifest
# 2. Using a different bundle (different Webpack config), so file is not hashed, and
# bundle_js_path will throw so the default path is used without a hash.
# 3. The third option of having the server bundle hashed and a different configuration than
# the client bundle is not supported for 2 reasons:
# a. The webpack manifest plugin would have a race condition where the same manifest.json
# is edited by both the webpack-dev-server
# b. There is no good reason to hash the server bundle name.
return @server_bundle_path if @server_bundle_path && !Rails.env.development?

bundle_name = ReactOnRails.configuration.server_bundle_js_file
@server_bundle_path = bundle_js_file_path(bundle_name)
end

def self.rsc_bundle_js_file_path
return @rsc_bundle_path if @rsc_bundle_path && !Rails.env.development?

# TODO: make it configurable
bundle_name = "rsc-bundle.js"
@rsc_bundle_path = bundle_js_file_path(bundle_name)
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

🛠️ Refactor suggestion

RSC bundle configuration should follow existing pattern for server bundle

The review suggestion is valid. Based on the configuration class implementation:

  1. The RSC bundle should follow the same configuration pattern as server_bundle_js_file, which is already a configurable attribute.
  2. The configuration class needs to be updated to include rsc_bundle_js_file as a new configuration option.
  3. Documentation is indeed sparse, with only a brief mention of React Server Components in the README.

Specific changes needed:

  • Add rsc_bundle_js_file to the Configuration class attributes
  • Add it to the default configuration in self.configuration
  • Update the ensure_webpack_generated_files_exists method to include the RSC bundle file
  • Add proper documentation about RSC bundle configuration and usage
🔗 Analysis chain

Add configuration support and documentation for RSC bundle

The RSC bundle implementation needs attention in two areas:

  1. The bundle name is hardcoded. As noted in the TODO, this should be configurable.
  2. Documentation about the RSC bundle's purpose and usage is missing.

Consider adding configuration support like this:

 def self.rsc_bundle_js_file_path
   return @rsc_bundle_path if @rsc_bundle_path && !Rails.env.development?

-  # TODO: make it configurable
-  bundle_name = "rsc-bundle.js"
+  bundle_name = ReactOnRails.configuration.rsc_bundle_js_file || "rsc-bundle.js"
   @rsc_bundle_path = bundle_js_file_path(bundle_name)
 end

Also, please add documentation explaining:

  • The purpose of the RSC bundle
  • When and how it's used
  • Configuration options

Let me gather more information about the configuration setup to provide a comprehensive verification.


Let me check the configuration class implementation to see the existing configuration pattern.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if RSC configuration is documented in README or configuration files
rg -g '*.md' -g '*.rb' -i 'rsc[_-]bundle|server[_-]components'

Length of output: 754


Script:

#!/bin/bash
# Check configuration class and related files
ast-grep --pattern 'class ReactOnRails::Configuration'

# Check for any existing bundle configuration patterns
rg -A 3 'bundle.*file.*=' lib/

Length of output: 1771


Script:

#!/bin/bash
# Check the configuration class implementation
cat lib/react_on_rails/configuration.rb

Length of output: 12660


def self.running_on_windows?
(/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil
end
Expand Down
11 changes: 10 additions & 1 deletion node_package/src/ReactOnRails.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ReactElement } from 'react';
import type { Readable } from 'stream';
import type { Readable, PassThrough } from 'stream';

import * as ClientStartup from './clientStartup';
import handleError from './handleError';
Expand Down Expand Up @@ -256,6 +256,15 @@ ctx.ReactOnRails = {
return streamServerRenderedReactComponent(options);
},

/**
* Used by server rendering by Rails
* @param options
AbanoubGhadban marked this conversation as resolved.
Show resolved Hide resolved
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
serverRenderRSCReactComponent(options: RenderParams): PassThrough {
throw new Error('serverRenderRSCReactComponent is supported in RSC bundle only.');
},

/**
* Used by Rails to catch errors in rendering
* @param options
Expand Down
94 changes: 94 additions & 0 deletions node_package/src/ReactOnRailsRSC.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type { ReactElement } from 'react';
// @ts-expect-error will define this module types later
AbanoubGhadban marked this conversation as resolved.
Show resolved Hide resolved
import { renderToReadableStream } from 'react-server-dom-webpack/server.edge';
import { PassThrough } from 'stream';
import fs from 'fs';

import { RenderParams } from './types';
import ComponentRegistry from './ComponentRegistry';
import createReactOutput from './createReactOutput';
import { isPromise, isServerRenderHash } from './isServerRenderResult';
import ReactOnRails from './ReactOnRails';

(async () => {
try {
// @ts-expect-error AsyncLocalStorage is not in the node types
globalThis.AsyncLocalStorage = (await import('node:async_hooks')).AsyncLocalStorage;
AbanoubGhadban marked this conversation as resolved.
Show resolved Hide resolved
} catch (e) {
console.log('AsyncLocalStorage not found');
}
})();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid assigning AsyncLocalStorage to globalThis; consider alternative approaches.

Assigning to globalThis.AsyncLocalStorage can pollute the global namespace and may lead to conflicts or unintended side effects. Additionally, using an asynchronous IIFE ((async () => { ... })()) for this purpose might introduce race conditions since other parts of the code might access AsyncLocalStorage before it's available.

Consider importing AsyncLocalStorage synchronously where needed without modifying globalThis. For example:

- (async () => {
-   try {
-     // @ts-expect-error AsyncLocalStorage is not in the node types
-     globalThis.AsyncLocalStorage = (await import('node:async_hooks')).AsyncLocalStorage;
-   } catch (e) {
-     console.log('AsyncLocalStorage not found');
-   }
- })();
+ try {
+   // @ts-expect-error AsyncLocalStorage is not in the node types
+   const { AsyncLocalStorage } = require('node:async_hooks');
+ } catch (e) {
+   console.log('AsyncLocalStorage not found');
+ }

This approach avoids modifying the global scope and ensures AsyncLocalStorage is available synchronously.

Committable suggestion skipped: line range outside the PR's diff.


const stringToStream = (str: string) => {
const stream = new PassThrough();
stream.push(str);
stream.push(null);
return stream;
};

const getBundleConfig = () => {
const bundleConfig = JSON.parse(fs.readFileSync('./public/webpack/development/react-client-manifest.json', 'utf8'));
// remove file:// from keys
const newBundleConfig: { [key: string]: any } = {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Specify a more precise type instead of any for newBundleConfig.

Using any defeats the purpose of TypeScript's type safety. Specifying a more precise type enhances code reliability and maintainability.

Apply this diff to specify a more accurate type:

-const newBundleConfig: { [key: string]: any } = {};
+const newBundleConfig: { [key: string]: typeof bundleConfig[keyof typeof bundleConfig] } = {};

This ensures that newBundleConfig has the same value types as the original bundleConfig. Adjust the type as needed based on the actual structure of the config.

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 eslint

[error] 32-32: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

for (const [key, value] of Object.entries(bundleConfig)) {
newBundleConfig[key.replace('file://', '')] = value;
}
return newBundleConfig;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve robustness of getBundleConfig function.

The function has several areas for improvement:

  1. The hardcoded path assumes a specific environment and directory structure
  2. Missing error handling for file read operations
  3. The any type should be more specific as flagged by ESLint

Consider applying these improvements:

-const getBundleConfig = () => {
+const getBundleConfig = (manifestPath = './public/webpack/development/react-client-manifest.json') => {
   try {
-    const bundleConfig = JSON.parse(fs.readFileSync('./public/webpack/development/react-client-manifest.json', 'utf8'));
+    const bundleConfig = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
     // remove file:// from keys
-    const newBundleConfig: { [key: string]: any } = {};
+    const newBundleConfig: { [key: string]: unknown } = {};
     for (const [key, value] of Object.entries(bundleConfig)) {
       newBundleConfig[key.replace('file://', '')] = value;
     }
     return newBundleConfig;
+  } catch (error) {
+    throw new Error(`Failed to read or parse manifest file: ${error}`);
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const getBundleConfig = () => {
const bundleConfig = JSON.parse(fs.readFileSync('./public/webpack/development/react-client-manifest.json', 'utf8'));
// remove file:// from keys
const newBundleConfig: { [key: string]: any } = {};
for (const [key, value] of Object.entries(bundleConfig)) {
newBundleConfig[key.replace('file://', '')] = value;
}
return newBundleConfig;
}
const getBundleConfig = (manifestPath = './public/webpack/development/react-client-manifest.json') => {
try {
const bundleConfig = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
// remove file:// from keys
const newBundleConfig: { [key: string]: unknown } = {};
for (const [key, value] of Object.entries(bundleConfig)) {
newBundleConfig[key.replace('file://', '')] = value;
}
return newBundleConfig;
} catch (error) {
throw new Error(`Failed to read or parse manifest file: ${error}`);
}
}
🧰 Tools
🪛 eslint

[error] 32-32: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


ReactOnRails.serverRenderRSCReactComponent = (options: RenderParams) => {
const { name, domNodeId, trace, props, railsContext, throwJsErrors } = options;

let renderResult: null | PassThrough = null;

try {
const componentObj = ComponentRegistry.get(name);
if (componentObj.isRenderer) {
throw new Error(`\
Detected a renderer while server rendering component '${name}'. \
See https://github.com/shakacode/react_on_rails#renderer-functions`);
}

const reactRenderingResult = createReactOutput({
componentObj,
domNodeId,
trace,
props,
railsContext,
});

if (isServerRenderHash(reactRenderingResult) || isPromise(reactRenderingResult)) {
throw new Error('Server rendering of streams is not supported for server render hashes or promises.');
}

renderResult = new PassThrough();
let finalValue = "";
const streamReader = renderToReadableStream(reactRenderingResult, getBundleConfig()).getReader();
const decoder = new TextDecoder();
const processStream = async () => {
const { done, value } = await streamReader.read();
if (done) {
renderResult?.push(null);
// @ts-expect-error value is not typed
debugConsole.log('value', finalValue);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

debugConsole is not defined; use console or import the appropriate logger.

The variable debugConsole is used but not defined or imported, which will lead to a ReferenceError at runtime.

Apply this diff to correct the issue:

-// @ts-expect-error value is not typed
-debugConsole.log('value', finalValue);
+// @ts-expect-error value is not typed
+console.log('value', finalValue);

Alternatively, if debugConsole is intended to be a custom logging utility, ensure it is properly imported or defined.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
debugConsole.log('value', finalValue);
// @ts-expect-error value is not typed
console.log('value', finalValue);

return;
}

finalValue += decoder.decode(value);
renderResult?.push(value);
processStream();
}
processStream();
Comment on lines +58 to +71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Replace recursion with iteration in processStream.

The current recursive implementation could lead to stack overflow for large streams. Consider using a loop instead.

-    const processStream = async () => {
-      const { done, value } = await streamReader.read();
-      if (done) {
-        renderResult?.push(null);
-        // @ts-expect-error value is not typed
-        debugConsole.log('value', finalValue);
-        return;
-      }
-
-      finalValue += decoder.decode(value);
-      renderResult?.push(value);
-      processStream();
-    }
-    processStream();
+    const processStream = async () => {
+      while (true) {
+        const { done, value } = await streamReader.read();
+        if (done) {
+          renderResult?.push(null);
+          // @ts-expect-error value is not typed
+          debugConsole.log('value', finalValue);
+          break;
+        }
+        finalValue += decoder.decode(value);
+        renderResult?.push(value);
+      }
+    }
+    await processStream();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const processStream = async () => {
const { done, value } = await streamReader.read();
if (done) {
renderResult?.push(null);
// @ts-expect-error value is not typed
debugConsole.log('value', finalValue);
return;
}
finalValue += decoder.decode(value);
renderResult?.push(value);
processStream();
}
processStream();
const processStream = async () => {
while (true) {
const { done, value } = await streamReader.read();
if (done) {
renderResult?.push(null);
// @ts-expect-error value is not typed
debugConsole.log('value', finalValue);
break;
}
finalValue += decoder.decode(value);
renderResult?.push(value);
}
}
await processStream();

} catch (e: unknown) {
if (throwJsErrors) {
throw e;
}

renderResult = stringToStream(`Error: ${e}`);
AbanoubGhadban marked this conversation as resolved.
Show resolved Hide resolved
}

return renderResult;
};

export * from './types';
export default ReactOnRails;
3 changes: 2 additions & 1 deletion node_package/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ReactElement, ReactNode, Component, ComponentType } from 'react';
import type { Readable } from 'stream';
import type { Readable, PassThrough } from 'stream';

// Don't import redux just for the type definitions
// See https://github.com/shakacode/react_on_rails/issues/1321
Expand Down Expand Up @@ -171,6 +171,7 @@ export interface ReactOnRails {
getComponent(name: string): RegisteredComponent;
serverRenderReactComponent(options: RenderParams): null | string | Promise<RenderResult>;
streamServerRenderedReactComponent(options: RenderParams): Readable;
serverRenderRSCReactComponent(options: RenderParams): PassThrough;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Missing test coverage for RSC implementation

The implementation requires test coverage for:

  • serverRenderRSCReactComponent method
  • rsc_react_component Ruby helper
  • Error handling scenarios

Key files needing tests:

  • node_package/src/ReactOnRailsRSC.ts
  • lib/react_on_rails/helper.rb
🔗 Analysis chain

Verify RSC implementation completeness

Since this is a WIP PR implementing RSC support, let's verify the complete implementation:


RSC implementation needs additional test coverage

The implementation appears incomplete as it requires:

  • Test coverage for the RSC-specific functionality
  • Tests for the Ruby helper method rsc_react_component
  • Tests for error handling in serverRenderRSCReactComponent
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify RSC implementation across the codebase

# Check for RSC implementation in ReactOnRails.ts
echo "Checking ReactOnRails implementation..."
rg "serverRenderRSCReactComponent" "node_package/src/ReactOnRails.ts" -A 5

# Check for corresponding Ruby helper methods
echo "Checking Ruby helpers..."
rg "rsc_react_component" "lib/react_on_rails/helper.rb" -A 5

# Check for test coverage
echo "Checking test coverage..."
fd -e spec.rb -e spec.ts -e test.ts | xargs rg "RSC|React Server Components"

Length of output: 2688


Script:

#!/bin/bash
# Check for RSC-specific test files
echo "Checking for RSC test files..."
fd -e spec.rb -e spec.ts -e test.ts | xargs rg "describe.*RSC|test.*RSC"

# Check for test coverage of rsc_react_component helper
echo "Checking Ruby helper tests..."
rg "rsc_react_component" "spec/" -A 5

# Check for error handling tests
echo "Checking error handling tests..."
rg "serverRenderRSCReactComponent.*Error" "spec/" -A 5

Length of output: 380

handleError(options: ErrorOptions): string | undefined;
buildConsoleReplay(): string;
registeredComponents(): Map<string, RegisteredComponent>;
Expand Down
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
"name": "react-on-rails",
"version": "15.0.0-alpha.1",
"description": "react-on-rails JavaScript for react_on_rails Ruby gem",
"main": "node_package/lib/ReactOnRails.js",
"exports": {
".": {
"rsc-server": "./node_package/lib/ReactOnRailsRSC.js",
"default": "./node_package/lib/ReactOnRails.js"
}
},
"directories": {
"doc": "docs"
},
Expand Down Expand Up @@ -41,6 +46,7 @@
"prop-types": "^15.8.1",
"react": "18.3.0-canary-670811593-20240322",
"react-dom": "18.3.0-canary-670811593-20240322",
"react-server-dom-webpack": "18.3.0-canary-670811593-20240322",
AbanoubGhadban marked this conversation as resolved.
Show resolved Hide resolved
"react-transform-hmr": "^1.0.4",
"redux": "^4.2.1",
"ts-jest": "^29.1.0",
Expand Down
Loading
Loading