-
-
Notifications
You must be signed in to change notification settings - Fork 630
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
Add support for console replay while streaming component #1647
Add support for console replay while streaming component #1647
Conversation
WalkthroughThe recent changes encompass updates to several files, enhancing the project's functionality by introducing new methods for streaming React components and improving console log handling during server rendering. Key updates include the addition of the Changes
Possibly related PRs
Suggested reviewers
Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Caution
Inline review comments failed to post
Actionable comments posted: 5
Outside diff range, codebase verification and nitpick comments (1)
package.json (1)
18-19
: Deprecated React API DetectedThe codebase contains deprecated React API usage that needs to be addressed for compatibility with React 18:
spec/dummy/client/app/startup/HelloWorldES5.jsx
: ReplaceReact.createClass
with ES6 class components or functional components with hooks.Please update these instances to ensure compatibility with the latest React version.
Analysis chain
Approve dependency upgrades but verify compatibility.
The upgrades to React and React DOM, along with their type definitions, are essential for staying current with the latest features. However, it's crucial to ensure that these changes are thoroughly tested to avoid potential breaking changes due to the major version upgrade.
The changes to the dependencies are approved.
Run the following script to verify compatibility with the new React version:
Also applies to: 42-43
Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Verify that no deprecated React APIs are used in the codebase. # Test: Search for deprecated React APIs. Expect: No occurrences. rg --type javascript --type typescript 'React\.createClass|React\.PropTypes'Length of output: 113
Script:
#!/bin/bash # Description: Verify that no deprecated React APIs are used in the codebase. # Test: Search for deprecated React APIs. Expect: No occurrences. rg --glob '*.js' --glob '*.jsx' --glob '*.ts' --glob '*.tsx' 'React\.createClass|React\.PropTypes'Length of output: 215
Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Files ignored due to path filters (1)
yarn.lock
is excluded by!**/yarn.lock
,!**/*.lock
Files selected for processing (16)
- .gitignore (1 hunks)
- jest.config.js (1 hunks)
- lib/react_on_rails/helper.rb (5 hunks)
- lib/react_on_rails/react_component/render_options.rb (1 hunks)
- lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb (3 hunks)
- node_package/src/ReactOnRails.ts (2 hunks)
- node_package/src/buildConsoleReplay.ts (2 hunks)
- node_package/src/serverRenderReactComponent.ts (2 hunks)
- node_package/src/types/index.ts (2 hunks)
- node_package/tests/ReactOnRails.test.js (1 hunks)
- node_package/tests/jest.setup.js (1 hunks)
- package.json (2 hunks)
- spec/dummy/config/webpack/alias.js (1 hunks)
- spec/dummy/config/webpack/commonWebpackConfig.js (1 hunks)
- spec/dummy/config/webpack/webpackConfig.js (1 hunks)
- spec/dummy/spec/helpers/react_on_rails_helper_spec.rb (2 hunks)
Files skipped from review due to trivial changes (1)
- .gitignore
Additional context used
Biome
node_package/src/buildConsoleReplay.ts
[error] 17-17: Use Array.isArray() instead of instanceof Array.
instanceof Array returns false for array-like objects and arrays from other execution contexts.
Unsafe fix: Use Array.isArray() instead.(lint/suspicious/useIsArray)
rubocop
lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb
[convention] 60-60: Use 2 (not -7) spaces for indentation.
(Layout/IndentationWidth)
[convention] 61-61: Align
else
withif
.(Layout/ElseAlignment)
[warning] 63-63:
end
at 63, 12 is not aligned withif
at 59, 21.(Layout/EndAlignment)
[convention] 78-78: Trailing whitespace detected.
(Layout/TrailingWhitespace)
lib/react_on_rails/helper.rb
[warning] 402-402: Useless assignment to variable -
rendered_html_stream
.(Lint/UselessAssignment)
[convention] 414-414: Line is too long. [130/120]
(Layout/LineLength)
Additional comments not posted (16)
jest.config.js (1)
4-4
: Approved: Addition of setupFiles in Jest configuration.The addition of
setupFiles
to the Jest configuration is correctly implemented and enhances the testing setup by allowing for the execution of initialization code before tests run.spec/dummy/config/webpack/alias.js (1)
7-7
: Approved: Addition of stream alias in Webpack configuration.The new alias mapping for
stream
tostream-browserify
is correctly implemented. This change enhances module resolution and compatibility with browser environments, facilitating the use of streaming functionalities in client-side applications.spec/dummy/config/webpack/webpackConfig.js (1)
7-7
: Approved: Addition of stream-browserify as a fallback.This change enhances client-side compatibility by providing a fallback for the
stream
module usingstream-browserify
. It's a good practice to ensure broader environment support.The code change is approved.
Consider verifying this fallback integration by testing in environments that do not natively support the Node.js
stream
module to ensure smooth functionality.node_package/src/buildConsoleReplay.ts (2)
14-21
: Approved: Enhanced flexibility in console message handling.The addition of the
skipFirstNumberOfMessages
parameter to bothconsoleReplay
andbuildConsoleReplay
functions enhances flexibility and user control over the output. This is a valuable feature for managing console logs more effectively during server rendering.The code changes are approved.
Also applies to: 42-43
Tools
Biome
[error] 17-17: Use Array.isArray() instead of instanceof Array.
instanceof Array returns false for array-like objects and arrays from other execution contexts.
Unsafe fix: Use Array.isArray() instead.(lint/suspicious/useIsArray)
21-21
: Fix required: Use Array.isArray() instead of instanceof Array.The use of
instanceof Array
can lead to incorrect type checks in certain contexts. Replace it withArray.isArray()
to ensure reliable and accurate type checking.Apply this diff to address the issue:
- if (!(console.history instanceof Array)) { + if (!Array.isArray(console.history)) { return ''; }Likely invalid or redundant comment.
spec/dummy/config/webpack/commonWebpackConfig.js (1)
44-44
: Approved: Addition of process/browser to ProvidePlugin.This change enhances client-side compatibility by polyfilling the
process
variable, which is crucial for libraries that rely on Node.js-like environments. It's a good practice to ensure broader library support.The code change is approved.
Consider verifying this polyfill integration by testing with libraries that rely on the
process
object to ensure smooth functionality.Verification successful
Verification Complete: process/browser Polyfill Integration Validated
The addition of
process: 'process/browser'
in the Webpack configuration is appropriate. It ensures compatibility for client-side code that relies on theprocess
object, as evidenced by its usage in various configuration files. This change supports the correct functioning of environment-dependent code in client-side applications.
- The
process
object is used in several Webpack and configuration files, indicating the necessity of this polyfill.Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Check the integration of process/browser polyfill in different environments. # Test: Ensure that libraries relying on the `process` object function correctly with this polyfill. echo "Manual testing required: Verify process/browser polyfill with libraries depending on the process object."Length of output: 218
Script:
#!/bin/bash # Search for occurrences of 'process' in the codebase to identify dependencies or code that might rely on this object. rg '\bprocess\b' -A 3Length of output: 22959
lib/react_on_rails/react_component/render_options.rb (1)
106-108
: Approve the addition of thestream?
method.The new
stream?
method is a useful addition for checking the streaming status of components. Ensure that this method is integrated and utilized properly in the context of component rendering.The addition of the
stream?
method is approved.Run the following script to verify the integration of the
stream?
method:Verification successful
The
stream?
method is properly integrated and utilized.The
stream?
method is effectively used in the codebase to manage rendering logic based on streaming status. Its integration inhelper.rb
andruby_embedded_java_script.rb
confirms its role in enhancing component rendering control.
- File:
lib/react_on_rails/helper.rb
- File:
lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb
Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Verify the usage of the `stream?` method in the codebase. # Test: Search for the usage of the `stream?` method. Expect: Proper integration in component rendering logic. rg --type ruby 'stream\?'Length of output: 625
node_package/tests/ReactOnRails.test.js (1)
22-30
: Approve the updated test setup but verify its effectiveness.The modifications to the test setup, including the dynamic creation of the root element and the updated assertion, are well-thought-out improvements. However, it's crucial to ensure that these changes accurately reflect the intended behavior and enhance the test's effectiveness.
The changes to the test setup are approved.
Run the following script to verify the effectiveness of the new test setup:
Verification successful
The test setup in
ReactOnRails.test.js
is consistent and effective.The usage of
document.createElement
andappendChild
in bothReactOnRails.test.js
andAuthenticity.test.js
demonstrates a consistent approach to DOM manipulation in the test suite. This consistency supports the effectiveness of the updated test setup in accurately reflecting the rendered output. No issues were found with the current implementation.Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Verify that the updated test setup accurately reflects the rendered output. # Test: Search for the usage of the new test setup in other test files. Expect: Consistent implementation and accurate test results. rg --type javascript 'document\.createElement|appendChild'Length of output: 95
Script:
#!/bin/bash # Description: Verify that the updated test setup accurately reflects the rendered output. # Test: Search for the usage of the new test setup in other test files. Expect: Consistent implementation and accurate test results. rg 'document\.createElement|appendChild' --glob '*.js'Length of output: 379
node_package/src/types/index.ts (2)
2-2
: Approved: Import statement forReadable
.The import of
Readable
from thestream
module is correctly added to support the new streaming functionality in theReactOnRails
interface.
141-141
: Approved: New methodstreamServerRenderedReactComponent
.The addition of the
streamServerRenderedReactComponent
method to theReactOnRails
interface is correctly implemented. This method enhances the server-side rendering capabilities by enabling streaming, which aligns with the PR's objectives.node_package/src/ReactOnRails.ts (1)
245-251
: Approved: Integration ofstreamServerRenderedReactComponent
intoReactOnRails
.The method
streamServerRenderedReactComponent
is correctly integrated into thectx.ReactOnRails
object, making it accessible as part of the module's API. The documentation comment provides clear guidance on its usage, which is beneficial for maintainability and developer understanding.lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb (2)
229-248
: Review new method for parsing and replaying console messages.The new method
parse_result_and_replay_console_messages
is crucial for handling console messages during server-side rendering. Ensure that the JSON parsing and error handling are robust, and that the method correctly logs messages to the server whenlogging_on_server
is enabled.The implementation seems robust, but consider adding more detailed logging or error handling if necessary.
79-83
: Ensure proper handling of streamed output.The transformation of the result to handle streamed output should be carefully reviewed to ensure that each chunk is processed correctly and that there are no performance implications or errors in handling the streamed data.
Run the following script to verify the proper handling of streamed output:
spec/dummy/spec/helpers/react_on_rails_helper_spec.rb (2)
10-10
: Confirm inclusion ofTagHelper
module.The inclusion of the
ActionView::Helpers::TagHelper
module should enhance the HTML tag generation capabilities of thePlainReactOnRailsHelper
class. Ensure that this inclusion does not introduce any conflicts or issues with existing methods.The change is approved, but verify that all methods that use HTML tag generation are still functioning as expected.
350-373
: Review new tests forrails_context_if_not_already_rendered
.The new tests for the
rails_context_if_not_already_rendered
method should ensure that it behaves correctly in scenarios where the Rails context has and has not been rendered. Verify that the tests cover all relevant cases and that they are correctly set up to simulate these scenarios.The tests appear to be well-written and cover the necessary scenarios. Ensure that they are integrated correctly with the rest of the test suite.
lib/react_on_rails/helper.rb (1)
396-419
: Ensure efficient handling of streamed content.The
build_react_component_result_for_server_streamed_content
method constructs the final output for streamed content. Verify that the method efficiently handles the first chunk differently from subsequent chunks and that it does not introduce any performance issues.Run the following script to verify the efficiency and correctness of the streamed content handling:
Tools
rubocop
[warning] 402-402: Useless assignment to variable -
rendered_html_stream
.(Lint/UselessAssignment)
[convention] 414-414: Line is too long. [130/120]
(Layout/LineLength)
Comments failed to post (5)
node_package/tests/jest.setup.js (1)
1-13: Approved: Addition of TextEncoder and TextDecoder polyfill in jest.setup.js.
The polyfill for
TextEncoder
andTextDecoder
is correctly implemented to ensure compatibility with the jsdom environment used in Jest tests. The conditional checks are well-placed to prevent unnecessary redefinitions.Consider monitoring future updates to jsdom that might include native support for
TextEncoder
andTextDecoder
, making this polyfill redundant.node_package/src/serverRenderReactComponent.ts (1)
177-236: Approved with suggestions: New function
streamServerRenderedReactComponent
.The implementation of
streamServerRenderedReactComponent
is robust, incorporating necessary error handling and stream transformations. The use ofReactDOMServer.renderToPipeableStream
is appropriate for streaming server-rendered components.Suggestions:
- Consider adding more detailed logging for debugging purposes, especially around the stream transformation process.
- Ensure comprehensive testing of error scenarios to validate the stream's error handling capabilities.
lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb (1)
59-63: Refactor conditional structure for clarity.
The conditional structure for handling streaming and non-streaming scenarios can be simplified to improve readability. Consider using a ternary operator or extracting the condition into a separate method if it grows more complex.
Apply this diff to refactor the conditional structure:
- result = if render_options.stream? - js_evaluator.eval_streaming_js(js_code, render_options) - else - js_evaluator.eval_js(js_code, render_options) - end + result = render_options.stream? ? js_evaluator.eval_streaming_js(js_code, render_options) : js_evaluator.eval_js(js_code, render_options)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.result = render_options.stream? ? js_evaluator.eval_streaming_js(js_code, render_options) : js_evaluator.eval_js(js_code, render_options)
Tools
rubocop
[convention] 60-60: Use 2 (not -7) spaces for indentation.
(Layout/IndentationWidth)
[convention] 61-61: Align
else
withif
.(Layout/ElseAlignment)
[warning] 63-63:
end
at 63, 12 is not aligned withif
at 59, 21.(Layout/EndAlignment)
lib/react_on_rails/helper.rb (2)
94-114: Review new method for streaming React components.
The new
stream_react_component
method introduces streaming capabilities for React components using Ruby'sFiber
. Ensure that the method handles errors appropriately and that the streaming process is efficient and robust.Consider adding more detailed error messages and handling potential exceptions during the streaming process more gracefully.
355-362: Optimize streaming content handling.
The
internal_stream_react_component
method modifies options to include a streaming flag and processes the component rendering accordingly. Ensure that the options modification does not inadvertently affect other parts of the codebase and that the streaming process is optimized for performance.Address the static analysis hint regarding the useless assignment to
rendered_html_stream
and ensure that all modifications to options are localized to this method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- One suggestion for readability
- I updated the description with the information from ChatGPT on how this works. Please double-check.
- Should we have any unit tests?
lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb
Outdated
Show resolved
Hide resolved
f38ed50
to
0c2313e
Compare
64ce293
to
e67efaf
Compare
d5d5d68
to
8937c5f
Compare
bcfddd6
to
6ad16f1
Compare
lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You have a great and extensive summary... but it doesn't seem to be for this PR :) Please update it to be relevant to these changes only. The changelog may not need to be updated if it's covered in the previous PR.
Approving with that understanding.
d1df75f
to
bd57e3d
Compare
bd57e3d
to
49547bb
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 9
🧹 Outside diff range and nitpick comments (2)
node_package/src/buildConsoleReplay.ts (1)
Line range hint 12-48
: Consider documenting streaming behavior.
Since this change is part of the streaming SSR implementation, consider adding JSDoc comments explaining how this function integrates with streaming, particularly how numberOfMessagesToSkip
relates to chunk processing.
Example documentation:
/**
* Generates a console replay script for server-side rendering.
* @param customConsoleHistory - Optional custom console history to use instead of global
* @param numberOfMessagesToSkip - Number of initial messages to skip, useful when streaming
* to avoid replaying console messages from previous chunks
* @returns A string containing JavaScript code to replay console messages
*/
CHANGELOG.md (1)
21-22
: Fix indentation in changelog entry
The bullet point indentation should be consistent with other entries in the changelog.
Apply this diff to fix the indentation:
### Added
- Added support for replaying console logs that occur during server rendering of streamed React components. This enables debugging of server-side rendering issues by capturing and displaying console output on the client and on the server output. [PR #1647](https://github.com/shakacode/react_on_rails/pull/1647) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
🧰 Tools
🪛 Markdownlint
22-22: Expected: 0; Actual: 1
Unordered list indentation
(MD007, ul-indent)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
📒 Files selected for processing (5)
- CHANGELOG.md (1 hunks)
- lib/react_on_rails/helper.rb (2 hunks)
- lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb (4 hunks)
- node_package/src/buildConsoleReplay.ts (2 hunks)
- node_package/src/serverRenderReactComponent.ts (3 hunks)
🧰 Additional context used
🪛 Markdownlint
CHANGELOG.md
22-22: Expected: 0; Actual: 1
Unordered list indentation
(MD007, ul-indent)
🔇 Additional comments (6)
node_package/src/buildConsoleReplay.ts (1)
12-12
: LGTM: Function signature changes maintain backward compatibility.
The addition of the optional numberOfMessagesToSkip
parameter with a default value of 0 is well-implemented, ensuring backward compatibility while adding new functionality.
Also applies to: 47-48
node_package/src/serverRenderReactComponent.ts (2)
2-2
: LGTM: Transform import added for streaming functionality.
The addition of Transform
to the stream imports is appropriate for implementing the new chunk processing functionality.
207-207
:
Consider potential race conditions in console history handling.
The current implementation might miss console messages if they are added between capturing the history and processing chunks. Consider implementing a locking mechanism or using a queue to ensure all console messages are captured accurately.
Consider this safer approach:
- const consoleHistory = console.history;
+ let currentHistory: typeof console.history = [];
+ const updateHistory = () => {
+ currentHistory = [...(console.history || [])];
+ };
+ updateHistory();
Also applies to: 225-230
CHANGELOG.md (1)
21-22
: Verify changelog entry matches PR objectives
The changelog entry accurately reflects the PR objectives regarding console replay support for streamed React components. The entry provides good context about the feature's purpose and benefits.
🧰 Tools
🪛 Markdownlint
22-22: Expected: 0; Actual: 1
Unordered list indentation
(MD007, ul-indent)
lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb (1)
59-63
: Conditional logic correctly handles streaming and non-streaming modes
The updated conditional statement appropriately selects between eval_streaming_js
and eval_js
based on the stream?
flag in render_options
. This ensures that the correct evaluation method is used for each rendering mode.
lib/react_on_rails/helper.rb (1)
438-453
: Stream transformation logic is correctly implemented
The implementation correctly handles the first chunk differently from subsequent chunks, ensuring that the component specification tag and Rails context are included only once. This approach optimizes the streaming of server-rendered content.
// console.history is a global polyfill used in server rendering. | ||
const consoleHistory = customConsoleHistory ?? console.history; | ||
|
||
if (!(Array.isArray(consoleHistory))) { | ||
return ''; | ||
} | ||
|
||
const lines = consoleHistory.map(msg => { | ||
const lines = consoleHistory.slice(numberOfMessagesToSkip).map(msg => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add validation for numberOfMessagesToSkip parameter.
The slice
operation could behave unexpectedly with negative numbers or values larger than the array length. Consider adding validation to ensure robust behavior.
Here's a suggested improvement:
+ if (numberOfMessagesToSkip < 0) {
+ throw new Error('numberOfMessagesToSkip must be non-negative');
+ }
+ if (numberOfMessagesToSkip > consoleHistory.length) {
+ return ''; // or throw error depending on desired behavior
+ }
const lines = consoleHistory.slice(numberOfMessagesToSkip).map(msg => {
📝 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.
const lines = consoleHistory.slice(numberOfMessagesToSkip).map(msg => { | |
if (numberOfMessagesToSkip < 0) { | |
throw new Error('numberOfMessagesToSkip must be non-negative'); | |
} | |
if (numberOfMessagesToSkip > consoleHistory.length) { | |
return ''; // or throw error depending on desired behavior | |
} | |
const lines = consoleHistory.slice(numberOfMessagesToSkip).map(msg => { |
const jsonChunk = JSON.stringify({ | ||
html: htmlChunk, | ||
consoleReplayScript, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enhance security and performance of JSON serialization.
The current implementation might be vulnerable to XSS attacks through console messages and could have performance issues with large chunks.
Consider these improvements:
-const jsonChunk = JSON.stringify({
- html: htmlChunk,
- consoleReplayScript,
-});
+const jsonChunk = JSON.stringify({
+ html: htmlChunk,
+ consoleReplayScript: consoleReplayScript.replace(/<|>/g, match =>
+ match === '<' ? '\\u003c' : '\\u003e'
+ ),
+}, null, 0);
📝 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.
const jsonChunk = JSON.stringify({ | |
html: htmlChunk, | |
consoleReplayScript, | |
}); | |
const jsonChunk = JSON.stringify({ | |
html: htmlChunk, | |
consoleReplayScript: consoleReplayScript.replace(/<|>/g, match => | |
match === '<' ? '\\u003c' : '\\u003e' | |
), | |
}, null, 0); |
ReactDOMServer.renderToPipeableStream(reactRenderingResult).pipe(transformStream); | ||
|
||
// TODO: Add console replay script to the stream | ||
renderResult = transformStream; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add error boundary and cleanup for streaming pipeline.
The streaming pipeline should handle errors gracefully and clean up resources properly.
Consider adding error handling:
-ReactDOMServer.renderToPipeableStream(reactRenderingResult).pipe(transformStream);
-renderResult = transformStream;
+const stream = ReactDOMServer.renderToPipeableStream(reactRenderingResult);
+stream.on('error', (error) => {
+ transformStream.destroy(error);
+});
+transformStream.on('error', (error) => {
+ console.error('Error during streaming:', error);
+});
+stream.pipe(transformStream);
+renderResult = transformStream;
📝 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.
ReactDOMServer.renderToPipeableStream(reactRenderingResult).pipe(transformStream); | |
// TODO: Add console replay script to the stream | |
renderResult = transformStream; | |
const stream = ReactDOMServer.renderToPipeableStream(reactRenderingResult); | |
stream.on('error', (error) => { | |
transformStream.destroy(error); | |
}); | |
transformStream.on('error', (error) => { | |
console.error('Error during streaming:', error); | |
}); | |
stream.pipe(transformStream); | |
renderResult = transformStream; |
const transformStream = new Transform({ | ||
transform(chunk, _, callback) { | ||
const htmlChunk = chunk.toString(); | ||
const consoleReplayScript = buildConsoleReplay(consoleHistory, previouslyReplayedConsoleMessages); | ||
previouslyReplayedConsoleMessages = consoleHistory?.length || 0; | ||
|
||
const jsonChunk = JSON.stringify({ | ||
html: htmlChunk, | ||
consoleReplayScript, | ||
}); | ||
|
||
this.push(jsonChunk); | ||
callback(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add error handling and backpressure management to transform stream.
The transform function lacks error handling and proper backpressure management, which could lead to memory issues with large streams or when errors occur during transformation.
Consider this improved implementation:
const transformStream = new Transform({
- transform(chunk, _, callback) {
+ async transform(chunk, encoding, callback) {
try {
- const htmlChunk = chunk.toString();
+ const htmlChunk = chunk.toString(encoding);
const consoleReplayScript = buildConsoleReplay(consoleHistory, previouslyReplayedConsoleMessages);
previouslyReplayedConsoleMessages = consoleHistory?.length || 0;
- const jsonChunk = JSON.stringify({
+ const jsonChunk = JSON.stringify({
html: htmlChunk,
consoleReplayScript,
- });
+ }) + '\n';
this.push(jsonChunk);
callback();
+ } catch (error) {
+ callback(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.
const transformStream = new Transform({ | |
transform(chunk, _, callback) { | |
const htmlChunk = chunk.toString(); | |
const consoleReplayScript = buildConsoleReplay(consoleHistory, previouslyReplayedConsoleMessages); | |
previouslyReplayedConsoleMessages = consoleHistory?.length || 0; | |
const jsonChunk = JSON.stringify({ | |
html: htmlChunk, | |
consoleReplayScript, | |
}); | |
this.push(jsonChunk); | |
callback(); | |
} | |
const transformStream = new Transform({ | |
async transform(chunk, encoding, callback) { | |
try { | |
const htmlChunk = chunk.toString(encoding); | |
const consoleReplayScript = buildConsoleReplay(consoleHistory, previouslyReplayedConsoleMessages); | |
previouslyReplayedConsoleMessages = consoleHistory?.length || 0; | |
const jsonChunk = JSON.stringify({ | |
html: htmlChunk, | |
consoleReplayScript, | |
}) + '\n'; | |
this.push(jsonChunk); | |
callback(); | |
} catch (error) { | |
callback(error); | |
} | |
} |
@@ -46,7 +46,7 @@ def reset_pool_if_server_bundle_was_modified | |||
# Note, js_code does not have to be based on React. | |||
# js_code MUST RETURN json stringify Object | |||
# Calling code will probably call 'html_safe' on return value before rendering to the view. | |||
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/PerceivedComplexity | |||
# rubocop:disable Metrics/CyclomaticComplexity |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consider refactoring to reduce cyclomatic complexity
Disabling RuboCop's Metrics/CyclomaticComplexity
for the exec_server_render_js
method suggests that it may have become too complex. Refactoring this method could improve maintainability and readability, allowing you to re-enable the RuboCop check.
return parse_result_and_replay_console_messages(result, render_options) unless render_options.stream? | ||
|
||
# TODO: merge with exec_server_render_js | ||
def exec_server_render_streaming_js(js_code, render_options, js_evaluator = nil) | ||
js_evaluator ||= self | ||
js_evaluator.eval_streaming_js(js_code, render_options) | ||
# Streamed component is returned as stream of strings. | ||
# We need to parse each chunk and replay the console messages. | ||
result.transform { |chunk| parse_result_and_replay_console_messages(chunk, render_options) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Verify that result
responds to transform
method
In the streaming case, the code calls result.transform { |chunk| ... }
. Ensure that result
is an object that responds to the transform
method. If transform
is not a valid method for result
, consider using map
or another appropriate enumerable method to process each chunk.
Apply this diff to use map
instead of transform
:
- result.transform { |chunk| parse_result_and_replay_console_messages(chunk, render_options) }
+ result.map { |chunk| parse_result_and_replay_console_messages(chunk, render_options) }
📝 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.
return parse_result_and_replay_console_messages(result, render_options) unless render_options.stream? | |
# TODO: merge with exec_server_render_js | |
def exec_server_render_streaming_js(js_code, render_options, js_evaluator = nil) | |
js_evaluator ||= self | |
js_evaluator.eval_streaming_js(js_code, render_options) | |
# Streamed component is returned as stream of strings. | |
# We need to parse each chunk and replay the console messages. | |
result.transform { |chunk| parse_result_and_replay_console_messages(chunk, render_options) } | |
return parse_result_and_replay_console_messages(result, render_options) unless render_options.stream? | |
# Streamed component is returned as stream of strings. | |
# We need to parse each chunk and replay the console messages. | |
result.map { |chunk| parse_result_and_replay_console_messages(chunk, render_options) } |
def parse_result_and_replay_console_messages(result_string, render_options) | ||
result = nil | ||
begin | ||
result = JSON.parse(result_string) | ||
rescue JSON::ParserError => e | ||
raise ReactOnRails::JsonParseError.new(parse_error: e, json: result_string) | ||
end | ||
|
||
if render_options.logging_on_server | ||
console_script = result["consoleReplayScript"] | ||
console_script_lines = console_script.split("\n") | ||
# Regular expression to match console.log or console.error calls with SERVER prefix | ||
re = /console\.(?:log|error)\.apply\(console, \["\[SERVER\] (?<msg>.*)"\]\);/ | ||
console_script_lines&.each do |line| | ||
match = re.match(line) | ||
# Log matched messages to Rails logger with react_on_rails prefix | ||
Rails.logger.info { "[react_on_rails] #{match[:msg]}" } if match | ||
end | ||
end | ||
result | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add nil checks for console_script
to prevent potential errors
In the parse_result_and_replay_console_messages
method, if result["consoleReplayScript"]
is nil
, calling split
on it will raise a NoMethodError
. It's advisable to check if console_script
is present before proceeding. Similarly, ensure that console_script_lines
is not nil
before iterating.
Apply this diff to add the necessary nil checks:
def parse_result_and_replay_console_messages(result_string, render_options)
result = nil
begin
result = JSON.parse(result_string)
rescue JSON::ParserError => e
raise ReactOnRails::JsonParseError.new(parse_error: e, json: result_string)
end
if render_options.logging_on_server
console_script = result["consoleReplayScript"]
+ return result unless console_script
console_script_lines = console_script.split("\n")
+ return result unless console_script_lines
# Regular expression to match console.log or console.error calls with SERVER prefix
re = /console\.(?:log|error)\.apply\(console, \["\[SERVER\] (?<msg>.*)"\]\);/
console_script_lines.each do |line|
match = re.match(line)
# Log matched messages to Rails logger with react_on_rails prefix
Rails.logger.info { "[react_on_rails] #{match[:msg]}" } if match
end
end
result
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.
def parse_result_and_replay_console_messages(result_string, render_options) | |
result = nil | |
begin | |
result = JSON.parse(result_string) | |
rescue JSON::ParserError => e | |
raise ReactOnRails::JsonParseError.new(parse_error: e, json: result_string) | |
end | |
if render_options.logging_on_server | |
console_script = result["consoleReplayScript"] | |
console_script_lines = console_script.split("\n") | |
# Regular expression to match console.log or console.error calls with SERVER prefix | |
re = /console\.(?:log|error)\.apply\(console, \["\[SERVER\] (?<msg>.*)"\]\);/ | |
console_script_lines&.each do |line| | |
match = re.match(line) | |
# Log matched messages to Rails logger with react_on_rails prefix | |
Rails.logger.info { "[react_on_rails] #{match[:msg]}" } if match | |
end | |
end | |
result | |
end | |
def parse_result_and_replay_console_messages(result_string, render_options) | |
result = nil | |
begin | |
result = JSON.parse(result_string) | |
rescue JSON::ParserError => e | |
raise ReactOnRails::JsonParseError.new(parse_error: e, json: result_string) | |
end | |
if render_options.logging_on_server | |
console_script = result["consoleReplayScript"] | |
return result unless console_script | |
console_script_lines = console_script.split("\n") | |
return result unless console_script_lines | |
# Regular expression to match console.log or console.error calls with SERVER prefix | |
re = /console\.(?:log|error)\.apply\(console, \["\[SERVER\] (?<msg>.*)"\]\);/ | |
console_script_lines.each do |line| | |
match = re.match(line) | |
# Log matched messages to Rails logger with react_on_rails prefix | |
Rails.logger.info { "[react_on_rails] #{match[:msg]}" } if match | |
end | |
end | |
result | |
end |
html_content = <<~HTML | ||
#{rendered_output} | ||
#{component_specification_tag} | ||
#{console_script} | ||
HTML | ||
html_content.strip.html_safe |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adjust indentation in HEREDOC to prevent unintended whitespace
The inconsistent indentation inside the HEREDOC may introduce unwanted whitespace in the generated HTML output. To ensure the rendered HTML is formatted correctly, align the content within the HEREDOC.
Apply this diff to correct the indentation:
html_content = <<~HTML
- #{rendered_output}
- #{component_specification_tag}
- #{console_script}
+#{rendered_output}
+#{component_specification_tag}
+#{console_script}
HTML
📝 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.
html_content = <<~HTML | |
#{rendered_output} | |
#{component_specification_tag} | |
#{console_script} | |
HTML | |
html_content.strip.html_safe | |
html_content = <<~HTML | |
#{rendered_output} | |
#{component_specification_tag} | |
#{console_script} | |
HTML | |
html_content.strip.html_safe |
rendered_html_stream:, | ||
component_specification_tag:, | ||
render_options: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ensure required keyword is used for parameter validation and consistency
In the build_react_component_result_for_server_streamed_content
method, the parameters are declared without the required
method. To ensure that these parameters are not nil
and to maintain consistency with other methods in the file, consider using the required
helper to validate them.
Apply this diff to enforce parameter validation:
def build_react_component_result_for_server_streamed_content(
- rendered_html_stream:,
- component_specification_tag:,
- render_options:
+ rendered_html_stream: required("rendered_html_stream"),
+ component_specification_tag: required("component_specification_tag"),
+ render_options: required("render_options")
)
📝 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.
rendered_html_stream:, | |
component_specification_tag:, | |
render_options: | |
rendered_html_stream: required("rendered_html_stream"), | |
component_specification_tag: required("component_specification_tag"), | |
render_options: required("render_options") |
Summary
This PR introduces the ability to replay console logs that happen in the react components streamed.
It makes every chunk returned from the server has the following format:
On the ruby side, the console replay script is returned in the HTML chunk to be replayed on the browser.
Description Auto-Generated by code-rabbit
Key Concepts in SSR and Streaming
Server-Side Rendering (SSR):
SSR generates the HTML for React components on the server and sends it to the client. This allows faster initial rendering on the client side because the HTML is already rendered when the page loads.
Streaming:
Instead of sending the entire HTML content at once after it has been fully rendered on the server, streaming allows the server to send partial chunks of HTML as they are rendered. This can improve the perceived load time for the client, as the client can begin processing the initial HTML while the server continues rendering the rest.
Streaming in the Code
The parts related to streaming are primarily in the
streamServerRenderedReactComponent
function and involve streaming the React component's HTML output back to the client.streamServerRenderedReactComponent
RenderParams
as input, which contains the details of the component to be rendered (name, props, DOM node ID, etc.).ReactDOMServer.renderToPipeableStream
ReactDOMServer.renderToPipeableStream
is a new React feature introduced in React 18 for streaming SSR.pipe
method connects this stream to another stream (transformStream
), allowing the rendered chunks to be processed and eventually sent to the client.Transform Stream
Transform
stream is used to process each chunk of HTML rendered by React.consoleReplayScript
).Error Handling in Streams
handleError
function generates an error message in the form of a string.stringToStream
, ensuring that even errors are streamed back to the client.stringToStream
stringToStream
creates a readable stream from a string.Summary of Streaming Flow
streamServerRenderedReactComponent
gets the component to be rendered and begins the rendering process usingReactDOMServer.renderToPipeableStream
.Transform
stream, which appends console logs and serializes the result as JSON.stringToStream
.This approach allows for efficient server-side rendering with streaming, which can improve performance and user experience by delivering parts of the content to the client as soon as they are ready, without waiting for the entire component to render.
This Ruby file is part of the
ReactOnRails
gem, which integrates React with Rails and enables server-side rendering (SSR) of React components. The streaming parts of this Ruby file relate closely to the streaming functionality in the TypeScript code you provided. Here's a breakdown of how the Ruby code handles SSR with a focus on streaming:Key Components of the Ruby File
react_component
Method:This method renders a React component on the server and returns the HTML to be injected into the Rails view. It supports both client-side rendering (CSR) and server-side rendering (SSR).
stream_react_component
Method:This method handles server-side rendering with streaming, which corresponds to the
streamServerRenderedReactComponent
function in the TypeScript code. Instead of waiting for the entire component to be rendered before sending it to the client, it sends chunks of HTML as they are rendered.Streaming Logic in Ruby
stream_react_component
Fiber Usage:
The
stream_react_component
method creates aFiber
to control the streaming of HTML chunks. Fibers in Ruby are lightweight, cooperative threads that allow the code to yield and resume execution, which is perfect for handling streaming data in a controlled way.Streaming Chunks:
The Fiber calls
internal_stream_react_component
, which returns a stream of chunks of HTML. The Fiber iterates through each chunk and sends it back to the Rails view as it becomes available.renderToPipeableStream
is used.Fiber Management:
The rendered chunks are managed by storing the rendering Fiber in an instance variable (
@rorp_rendering_fibers
). The first chunk is sent by callingrendering_fiber.resume
.internal_stream_react_component
Options Modification:
This method ensures that the
stream?
option is set totrue
, signaling that the component should be streamed rather than fully rendered before being sent to the client.Streamed Content:
The result of
internal_react_component
is passed tobuild_react_component_result_for_server_streamed_content
, which prepares the HTML chunks to be streamed back to the client.build_react_component_result_for_server_streamed_content
Transforming Streamed Chunks:
This method takes the stream of chunks from the rendered React component and processes them one by one. The first chunk is special because it contains the initial HTML along with the component specification tag (metadata about the component).
Subsequent Chunks:
For subsequent chunks, the HTML is processed and appended to the response, similar to how the
Transform
stream in the TypeScript code appends the console logs and wraps the HTML in JSON.Console Replay:
Each chunk includes the option to replay console messages (
consoleReplayScript
), as in the TypeScript code, where each HTML chunk includes embedded console logs.Relation to TypeScript Streaming Logic
Rendering Flow:
Both the Ruby and TypeScript code follow a similar flow for streaming:
Chunk Processing:
The Ruby method
build_react_component_result_for_server_streamed_content
closely mirrors the functionality of theTransform
stream in the TypeScript code. Both process each chunk of HTML, append necessary metadata (like console logs), and send it to the client.Fiber and Stream Handling:
The use of Fibers in Ruby allows the server to stream the chunks in a non-blocking manner, similar to how the Node.js
renderToPipeableStream
function allows React to stream chunks without waiting for the entire component to finish rendering.Summary
The Ruby file is responsible for managing the server-side rendering of React components within a Rails app, including support for streaming the rendered HTML. It parallels the TypeScript streaming logic by using Fibers to control the flow of streamed HTML chunks, ensuring that the client receives content as it becomes available. Both the Ruby and TypeScript implementations focus on improving performance by enabling partial responses to be sent during rendering, reducing the time to the first byte for the client.
Pull Request checklist
Remove this line after checking all the items here. If the item is not applicable to the PR, both check it out and wrap it by
~
.Add the CHANGELOG entry at the top of the file.
Other Information
Remove this paragraph and mention any other important and relevant information such as benchmarks.
This change is
Summary by CodeRabbit
Summary by CodeRabbit
New Features
ReactOnRails
module with a new helper for processing streamed React components.Bug Fixes
Documentation
Dependency Updates
Chores
.gitignore
file for cleaner version control, targeting IDE-related files.