Skip to content

Commit

Permalink
pass console messages from server to client and replay them
Browse files Browse the repository at this point in the history
  • Loading branch information
AbanoubGhadban committed Oct 20, 2024
1 parent 8937c5f commit 1b8cbf0
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 50 deletions.
31 changes: 15 additions & 16 deletions lib/react_on_rails/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -435,26 +435,24 @@ def build_react_component_result_for_server_streamed_content(
component_specification_tag: required("component_specification_tag"),
render_options: required("render_options")
)
content_tag_options_html_tag = render_options.html_options[:tag] || "div"
# The component_specification_tag is appended to the first chunk
# We need to pass it early with the first chunk because it's needed in hydration
# We need to make sure that client can hydrate the app early even before all components are streamed
is_first_chunk = true
rendered_html_stream = rendered_html_stream.transform do |chunk|
rendered_html_stream = rendered_html_stream.transform do |chunk_json_result|
if is_first_chunk
is_first_chunk = false
html_content = <<-HTML
#{rails_context_if_not_already_rendered}
#{component_specification_tag}
<#{content_tag_options_html_tag} id="#{render_options.dom_id}">#{chunk}</#{content_tag_options_html_tag}>
HTML
next html_content.strip
next build_react_component_result_for_server_rendered_string(
server_rendered_html: chunk_json_result["html"],
component_specification_tag: component_specification_tag,
console_script: chunk_json_result["consoleReplayScript"],
render_options: render_options
)
end
chunk
end

rendered_html_stream.transform(&:html_safe)
# TODO: handle console logs
result_console_script = render_options.replay_console ? chunk_json_result["consoleReplayScript"] : ""
# No need to prepend component_specification_tag or add rails context again as they're already included in the first chunk
compose_react_component_html_with_spec_and_console(
"", chunk_json_result["html"], result_console_script
)
end
end

def build_react_component_result_for_server_rendered_hash(
Expand Down Expand Up @@ -493,11 +491,12 @@ def build_react_component_result_for_server_rendered_hash(

def compose_react_component_html_with_spec_and_console(component_specification_tag, rendered_output, console_script)
# IMPORTANT: Ensure that we mark string as html_safe to avoid escaping.
<<~HTML.html_safe
html_content = <<~HTML
#{rendered_output}
#{component_specification_tag}
#{console_script}
HTML
html_content.strip.html_safe
end

def rails_context_if_not_already_rendered
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@ def exec_server_render_js(js_code, render_options, js_evaluator = nil)
@file_index += 1
end
begin
json_string = js_evaluator.eval_js(js_code, render_options)
result = if render_options.stream?
js_evaluator.eval_streaming_js(js_code, render_options)
else
js_evaluator.eval_js(js_code, render_options)
end
rescue StandardError => err
msg = <<~MSG
Error evaluating server bundle. Check your webpack configuration.
Expand All @@ -71,33 +75,15 @@ def exec_server_render_js(js_code, render_options, js_evaluator = nil)
end
raise ReactOnRails::Error, msg, err.backtrace
end
result = nil
begin
result = JSON.parse(json_string)
rescue JSON::ParserError => e
raise ReactOnRails::JsonParseError.new(parse_error: e, json: json_string)
end

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

if render_options.logging_on_server
console_script = result["consoleReplayScript"]
console_script_lines = console_script.split("\n")
console_script_lines = console_script_lines[2..-2]
re = /console\.(?:log|error)\.apply\(console, \["\[SERVER\] (?<msg>.*)"\]\);/
console_script_lines&.each do |line|
match = re.match(line)
Rails.logger.info { "[react_on_rails] #{match[:msg]}" } if match
end
end
result
# 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) }
end
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/PerceivedComplexity

# 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)
end

def trace_js_code_used(msg, js_code, file_name = "tmp/server-generated.js", force: false)
return unless ReactOnRails.configuration.trace || force

Expand Down Expand Up @@ -239,6 +225,27 @@ def file_url_to_string(url)
msg = "file_url_to_string #{url} failed\nError is: #{e}"
raise ReactOnRails::Error, msg
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"]
console_script_lines = console_script.split("\n")
console_script_lines = console_script_lines[2..-2]
re = /console\.(?:log|error)\.apply\(console, \["\[SERVER\] (?<msg>.*)"\]\);/
console_script_lines&.each do |line|
match = re.match(line)
Rails.logger.info { "[react_on_rails] #{match[:msg]}" } if match
end
end
result
end
end
# rubocop:enable Metrics/ClassLength
end
Expand Down
8 changes: 4 additions & 4 deletions node_package/src/buildConsoleReplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ declare global {
}
}

export function consoleReplay(customConsoleHistory: typeof console['history'] | undefined = undefined): string {
export function consoleReplay(customConsoleHistory: typeof console['history'] | undefined = undefined, skipFirstNumberOfMessages: number = 0): string {
// 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(skipFirstNumberOfMessages).map(msg => {
const stringifiedList = msg.arguments.map(arg => {
let val: string;
try {
Expand All @@ -44,6 +44,6 @@ export function consoleReplay(customConsoleHistory: typeof console['history'] |
return lines.join('\n');
}

export default function buildConsoleReplay(customConsoleHistory: typeof console['history'] | undefined = undefined): string {
return RenderUtils.wrapInScriptTags('consoleReplayLog', consoleReplay(customConsoleHistory));
export default function buildConsoleReplay(customConsoleHistory: typeof console['history'] | undefined = undefined, skipFirstNumberOfMessages: number = 0): string {
return RenderUtils.wrapInScriptTags('consoleReplayLog', consoleReplay(customConsoleHistory, skipFirstNumberOfMessages));
}
29 changes: 23 additions & 6 deletions node_package/src/serverRenderReactComponent.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ReactDOMServer from 'react-dom/server';
import { PassThrough, Readable } from 'stream';
import { PassThrough, Readable, Transform } from 'stream';
import type { ReactElement } from 'react';

import ComponentRegistry from './ComponentRegistry';
Expand Down Expand Up @@ -204,6 +204,7 @@ export const streamServerRenderedReactComponent = (options: RenderParams): Reada
const { name: componentName, domNodeId, trace, props, railsContext, throwJsErrors } = options;

let renderResult: null | Readable = null;
let previouslyReplayedConsoleMessages: number = 0;

try {
const componentObj = ComponentRegistry.get(componentName);
Expand All @@ -221,12 +222,28 @@ export const streamServerRenderedReactComponent = (options: RenderParams): Reada
throw new Error('Server rendering of streams is not supported for server render hashes or promises.');
}

const renderStream = new PassThrough();
ReactDOMServer.renderToPipeableStream(reactRenderingResult).pipe(renderStream);
renderResult = renderStream;
const consoleHistory = console.history;
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();
}
});

ReactDOMServer.renderToPipeableStream(reactRenderingResult)
.pipe(transformStream);

// TODO: Add console replay script to the stream
} catch (e) {
renderResult = transformStream;
} catch (e: unknown) {
if (throwJsErrors) {
throw e;
}
Expand Down

0 comments on commit 1b8cbf0

Please sign in to comment.