Skip to content

Commit 1b8cbf0

Browse files
pass console messages from server to client and replay them
1 parent 8937c5f commit 1b8cbf0

File tree

4 files changed

+73
-50
lines changed

4 files changed

+73
-50
lines changed

lib/react_on_rails/helper.rb

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -435,26 +435,24 @@ def build_react_component_result_for_server_streamed_content(
435435
component_specification_tag: required("component_specification_tag"),
436436
render_options: required("render_options")
437437
)
438-
content_tag_options_html_tag = render_options.html_options[:tag] || "div"
439-
# The component_specification_tag is appended to the first chunk
440-
# We need to pass it early with the first chunk because it's needed in hydration
441-
# We need to make sure that client can hydrate the app early even before all components are streamed
442438
is_first_chunk = true
443-
rendered_html_stream = rendered_html_stream.transform do |chunk|
439+
rendered_html_stream = rendered_html_stream.transform do |chunk_json_result|
444440
if is_first_chunk
445441
is_first_chunk = false
446-
html_content = <<-HTML
447-
#{rails_context_if_not_already_rendered}
448-
#{component_specification_tag}
449-
<#{content_tag_options_html_tag} id="#{render_options.dom_id}">#{chunk}</#{content_tag_options_html_tag}>
450-
HTML
451-
next html_content.strip
442+
next build_react_component_result_for_server_rendered_string(
443+
server_rendered_html: chunk_json_result["html"],
444+
component_specification_tag: component_specification_tag,
445+
console_script: chunk_json_result["consoleReplayScript"],
446+
render_options: render_options
447+
)
452448
end
453-
chunk
454-
end
455449

456-
rendered_html_stream.transform(&:html_safe)
457-
# TODO: handle console logs
450+
result_console_script = render_options.replay_console ? chunk_json_result["consoleReplayScript"] : ""
451+
# No need to prepend component_specification_tag or add rails context again as they're already included in the first chunk
452+
compose_react_component_html_with_spec_and_console(
453+
"", chunk_json_result["html"], result_console_script
454+
)
455+
end
458456
end
459457

460458
def build_react_component_result_for_server_rendered_hash(
@@ -493,11 +491,12 @@ def build_react_component_result_for_server_rendered_hash(
493491

494492
def compose_react_component_html_with_spec_and_console(component_specification_tag, rendered_output, console_script)
495493
# IMPORTANT: Ensure that we mark string as html_safe to avoid escaping.
496-
<<~HTML.html_safe
494+
html_content = <<~HTML
497495
#{rendered_output}
498496
#{component_specification_tag}
499497
#{console_script}
500498
HTML
499+
html_content.strip.html_safe
501500
end
502501

503502
def rails_context_if_not_already_rendered

lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,11 @@ def exec_server_render_js(js_code, render_options, js_evaluator = nil)
5656
@file_index += 1
5757
end
5858
begin
59-
json_string = js_evaluator.eval_js(js_code, render_options)
59+
result = if render_options.stream?
60+
js_evaluator.eval_streaming_js(js_code, render_options)
61+
else
62+
js_evaluator.eval_js(js_code, render_options)
63+
end
6064
rescue StandardError => err
6165
msg = <<~MSG
6266
Error evaluating server bundle. Check your webpack configuration.
@@ -71,33 +75,15 @@ def exec_server_render_js(js_code, render_options, js_evaluator = nil)
7175
end
7276
raise ReactOnRails::Error, msg, err.backtrace
7377
end
74-
result = nil
75-
begin
76-
result = JSON.parse(json_string)
77-
rescue JSON::ParserError => e
78-
raise ReactOnRails::JsonParseError.new(parse_error: e, json: json_string)
79-
end
78+
79+
return parse_result_and_replay_console_messages(result, render_options) unless render_options.stream?
8080

81-
if render_options.logging_on_server
82-
console_script = result["consoleReplayScript"]
83-
console_script_lines = console_script.split("\n")
84-
console_script_lines = console_script_lines[2..-2]
85-
re = /console\.(?:log|error)\.apply\(console, \["\[SERVER\] (?<msg>.*)"\]\);/
86-
console_script_lines&.each do |line|
87-
match = re.match(line)
88-
Rails.logger.info { "[react_on_rails] #{match[:msg]}" } if match
89-
end
90-
end
91-
result
81+
# Streamed component is returned as stream of strings.
82+
# We need to parse each chunk and replay the console messages.
83+
result.transform { |chunk| parse_result_and_replay_console_messages(chunk, render_options) }
9284
end
9385
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/PerceivedComplexity
9486

95-
# TODO: merge with exec_server_render_js
96-
def exec_server_render_streaming_js(js_code, render_options, js_evaluator = nil)
97-
js_evaluator ||= self
98-
js_evaluator.eval_streaming_js(js_code, render_options)
99-
end
100-
10187
def trace_js_code_used(msg, js_code, file_name = "tmp/server-generated.js", force: false)
10288
return unless ReactOnRails.configuration.trace || force
10389

@@ -239,6 +225,27 @@ def file_url_to_string(url)
239225
msg = "file_url_to_string #{url} failed\nError is: #{e}"
240226
raise ReactOnRails::Error, msg
241227
end
228+
229+
def parse_result_and_replay_console_messages(result_string, render_options)
230+
result = nil
231+
begin
232+
result = JSON.parse(result_string)
233+
rescue JSON::ParserError => e
234+
raise ReactOnRails::JsonParseError.new(parse_error: e, json: result_string)
235+
end
236+
237+
if render_options.logging_on_server
238+
console_script = result["consoleReplayScript"]
239+
console_script_lines = console_script.split("\n")
240+
console_script_lines = console_script_lines[2..-2]
241+
re = /console\.(?:log|error)\.apply\(console, \["\[SERVER\] (?<msg>.*)"\]\);/
242+
console_script_lines&.each do |line|
243+
match = re.match(line)
244+
Rails.logger.info { "[react_on_rails] #{match[:msg]}" } if match
245+
end
246+
end
247+
result
248+
end
242249
end
243250
# rubocop:enable Metrics/ClassLength
244251
end

node_package/src/buildConsoleReplay.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ declare global {
99
}
1010
}
1111

12-
export function consoleReplay(customConsoleHistory: typeof console['history'] | undefined = undefined): string {
12+
export function consoleReplay(customConsoleHistory: typeof console['history'] | undefined = undefined, skipFirstNumberOfMessages: number = 0): string {
1313
// console.history is a global polyfill used in server rendering.
1414
const consoleHistory = customConsoleHistory ?? console.history;
1515

1616
if (!(Array.isArray(consoleHistory))) {
1717
return '';
1818
}
1919

20-
const lines = consoleHistory.map(msg => {
20+
const lines = consoleHistory.slice(skipFirstNumberOfMessages).map(msg => {
2121
const stringifiedList = msg.arguments.map(arg => {
2222
let val: string;
2323
try {
@@ -44,6 +44,6 @@ export function consoleReplay(customConsoleHistory: typeof console['history'] |
4444
return lines.join('\n');
4545
}
4646

47-
export default function buildConsoleReplay(customConsoleHistory: typeof console['history'] | undefined = undefined): string {
48-
return RenderUtils.wrapInScriptTags('consoleReplayLog', consoleReplay(customConsoleHistory));
47+
export default function buildConsoleReplay(customConsoleHistory: typeof console['history'] | undefined = undefined, skipFirstNumberOfMessages: number = 0): string {
48+
return RenderUtils.wrapInScriptTags('consoleReplayLog', consoleReplay(customConsoleHistory, skipFirstNumberOfMessages));
4949
}

node_package/src/serverRenderReactComponent.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import ReactDOMServer from 'react-dom/server';
2-
import { PassThrough, Readable } from 'stream';
2+
import { PassThrough, Readable, Transform } from 'stream';
33
import type { ReactElement } from 'react';
44

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

206206
let renderResult: null | Readable = null;
207+
let previouslyReplayedConsoleMessages: number = 0;
207208

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

224-
const renderStream = new PassThrough();
225-
ReactDOMServer.renderToPipeableStream(reactRenderingResult).pipe(renderStream);
226-
renderResult = renderStream;
225+
const consoleHistory = console.history;
226+
const transformStream = new Transform({
227+
transform(chunk, _, callback) {
228+
const htmlChunk = chunk.toString();
229+
const consoleReplayScript = buildConsoleReplay(consoleHistory, previouslyReplayedConsoleMessages);
230+
previouslyReplayedConsoleMessages = consoleHistory?.length || 0;
231+
232+
const jsonChunk = JSON.stringify({
233+
html: htmlChunk,
234+
consoleReplayScript,
235+
});
236+
237+
this.push(jsonChunk);
238+
callback();
239+
}
240+
});
241+
242+
ReactDOMServer.renderToPipeableStream(reactRenderingResult)
243+
.pipe(transformStream);
227244

228-
// TODO: Add console replay script to the stream
229-
} catch (e) {
245+
renderResult = transformStream;
246+
} catch (e: unknown) {
230247
if (throwJsErrors) {
231248
throw e;
232249
}

0 commit comments

Comments
 (0)