Skip to content

Commit ce37122

Browse files
pass console messages from server to client and replay them
1 parent f38ed50 commit ce37122

File tree

4 files changed

+71
-50
lines changed

4 files changed

+71
-50
lines changed

lib/react_on_rails/helper.rb

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -398,26 +398,24 @@ def build_react_component_result_for_server_streamed_content(
398398
component_specification_tag: required("component_specification_tag"),
399399
render_options: required("render_options")
400400
)
401-
content_tag_options_html_tag = render_options.html_options[:tag] || "div"
402-
# The component_specification_tag is appended to the first chunk
403-
# We need to pass it early with the first chunk because it's needed in hydration
404-
# We need to make sure that client can hydrate the app early even before all components are streamed
405401
is_first_chunk = true
406-
rendered_html_stream = rendered_html_stream.transform do |chunk|
402+
rendered_html_stream = rendered_html_stream.transform do |chunk_json_result|
407403
if is_first_chunk
408404
is_first_chunk = false
409-
html_content = <<-HTML
410-
#{rails_context_if_not_already_rendered}
411-
#{component_specification_tag}
412-
<#{content_tag_options_html_tag} id="#{render_options.dom_id}">#{chunk}</#{content_tag_options_html_tag}>
413-
HTML
414-
next html_content.strip
405+
next build_react_component_result_for_server_rendered_string(
406+
server_rendered_html: chunk_json_result["html"],
407+
component_specification_tag: component_specification_tag,
408+
console_script: chunk_json_result["consoleReplayScript"],
409+
render_options: render_options
410+
)
415411
end
416-
chunk
417-
end
418412

419-
rendered_html_stream.transform(&:html_safe)
420-
# TODO: handle console logs
413+
result_console_script = render_options.replay_console ? chunk_json_result["consoleReplayScript"] : ""
414+
# No need to prepend component_specification_tag or add rails context again as they're already included in the first chunk
415+
compose_react_component_html_with_spec_and_console(
416+
"", chunk_json_result["html"], result_console_script
417+
)
418+
end
421419
end
422420

423421
def build_react_component_result_for_server_rendered_hash(
@@ -456,11 +454,12 @@ def build_react_component_result_for_server_rendered_hash(
456454

457455
def compose_react_component_html_with_spec_and_console(component_specification_tag, rendered_output, console_script)
458456
# IMPORTANT: Ensure that we mark string as html_safe to avoid escaping.
459-
<<~HTML.html_safe
457+
html_content = <<~HTML
460458
#{rendered_output}
461459
#{component_specification_tag}
462460
#{console_script}
463461
HTML
462+
html_content.strip.html_safe
464463
end
465464

466465
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
@@ -11,14 +11,14 @@ declare global {
1111
}
1212
}
1313

14-
export function consoleReplay(): string {
14+
export function consoleReplay(skipFirstNumberOfMessages: number = 0): string {
1515
// console.history is a global polyfill used in server rendering.
1616
// $FlowFixMe
1717
if (!(console.history instanceof Array)) {
1818
return '';
1919
}
2020

21-
const lines = console.history.map(msg => {
21+
const lines = console.history.slice(skipFirstNumberOfMessages).map(msg => {
2222
const stringifiedList = msg.arguments.map(arg => {
2323
let val;
2424
try {
@@ -39,6 +39,6 @@ export function consoleReplay(): string {
3939
return lines.join('\n');
4040
}
4141

42-
export default function buildConsoleReplay(): string {
43-
return RenderUtils.wrapInScriptTags('consoleReplayLog', consoleReplay());
42+
export default function buildConsoleReplay(skipFirstNumberOfMessages: number = 0): string {
43+
return RenderUtils.wrapInScriptTags('consoleReplayLog', consoleReplay(skipFirstNumberOfMessages));
4444
}

node_package/src/serverRenderReactComponent.ts

Lines changed: 21 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';
@@ -178,6 +178,7 @@ export const streamServerRenderedReactComponent = (options: RenderParams): Reada
178178
const { name, domNodeId, trace, props, railsContext, throwJsErrors } = options;
179179

180180
let renderResult: null | Readable = null;
181+
let previouslyReplayedConsoleMessages: number = 0;
181182

182183
try {
183184
const componentObj = ComponentRegistry.get(name);
@@ -199,12 +200,26 @@ See https://github.com/shakacode/react_on_rails#renderer-functions`);
199200
throw new Error('Server rendering of streams is not supported for server render hashes or promises.');
200201
}
201202

202-
const renderStream = new PassThrough();
203-
ReactDOMServer.renderToPipeableStream(reactRenderingResult).pipe(renderStream);
204-
renderResult = renderStream;
203+
const transformStream = new Transform({
204+
transform(chunk, _, callback) {
205+
const htmlChunk = chunk.toString();
206+
const consoleReplayScript = buildConsoleReplay(previouslyReplayedConsoleMessages);
207+
previouslyReplayedConsoleMessages = console.history?.length || 0;
208+
209+
const jsonChunk = JSON.stringify({
210+
html: htmlChunk,
211+
consoleReplayScript,
212+
});
213+
214+
this.push(jsonChunk);
215+
callback();
216+
}
217+
});
218+
219+
ReactDOMServer.renderToPipeableStream(reactRenderingResult)
220+
.pipe(transformStream);
205221

206-
// TODO: Add console replay script to the stream
207-
// Ensure to avoid console messages leaking between different components rendering
222+
renderResult = transformStream;
208223
} catch (e: any) {
209224
if (throwJsErrors) {
210225
throw e;

0 commit comments

Comments
 (0)