Skip to content

Commit 381d8c8

Browse files
committed
feat(SprocketsRenderer) add error handling and console replay
1 parent 799ffce commit 381d8c8

File tree

2 files changed

+78
-9
lines changed

2 files changed

+78
-9
lines changed

lib/react/server_rendering/sprockets_renderer.rb

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,29 @@ module React
22
module ServerRendering
33
class SprocketsRenderer
44
def initialize(options={})
5-
filenames = options.fetch(:setup, ["react.js", "components.js"])
65
@replay_console = options.fetch(:replay_console, true)
76

8-
js_code = SetupJavascript.new(filenames).to_s
7+
filenames = options.fetch(:files, ["react.js", "components.js"])
8+
js_code = SetupJavascript.new(filenames).code
99
@context = ExecJS.compile(js_code)
1010
end
1111

1212
def render(component_name, props)
1313
if !props.is_a?(String)
1414
props = props.to_json
1515
end
16+
1617
js_code = <<-JS
1718
(function () {
1819
var result = React.renderToString(React.createElement(#{component_name}, #{props}));
20+
#{@replay_console ? CONSOLE_REPLAY : ""}
1921
return result;
2022
})()
2123
JS
24+
2225
@context.eval(js_code).html_safe
23-
# handle error
26+
rescue ExecJS::ProgramError => err
27+
raise PrerenderError.new(component_name, props, err)
2428
end
2529

2630
class SetupJavascript
@@ -29,16 +33,43 @@ class SetupJavascript
2933
var self = self || this;
3034
var window = window || this;
3135
JS
36+
37+
attr_reader :code
38+
3239
def initialize(filenames)
33-
js_code = ""
40+
@code = GLOBAL_WRAPPER + CONSOLE_POLYFILL
3441
filenames.each do |filename|
35-
js_code << ::Rails.application.assets[filename].to_s
42+
@code << ::Rails.application.assets[filename].to_s
3643
end
37-
@wrapped_code = GLOBAL_WRAPPER + js_code
3844
end
45+
end
46+
47+
CONSOLE_POLYFILL = <<-JS
48+
var console = { history: [] };
49+
['error', 'log', 'info', 'warn'].forEach(function (fn) {
50+
console[fn] = function () {
51+
console.history.push({level: fn, arguments: Array.prototype.slice.call(arguments)});
52+
};
53+
});
54+
JS
55+
56+
CONSOLE_REPLAY = <<-JS
57+
(function (history) {
58+
if (history && history.length > 0) {
59+
result += '\\n<scr'+'ipt>';
60+
history.forEach(function (msg) {
61+
result += '\\nconsole.' + msg.level + '.apply(console, ' + JSON.stringify(msg.arguments) + ');';
62+
});
63+
result += '\\n</scr'+'ipt>';
64+
}
65+
})(console.history);
66+
JS
3967

40-
def to_s
41-
@wrapped_code
68+
class PrerenderError < RuntimeError
69+
def initialize(component_name, props, js_message)
70+
message = ["Encountered error \"#{js_message}\" when prerendering #{component_name} with #{props}",
71+
js_message.backtrace.join("\n")].join("\n")
72+
super(message)
4273
end
4374
end
4475
end

test/react/server_rendering/sprockets_renderer_test.rb

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,45 @@ class SprocketsRendererTest < ActiveSupport::TestCase
77

88
test '#render returns HTML' do
99
result = @renderer.render("Todo", {todo: "write tests"})
10-
# skip reactid & checksum:
1110
assert_match(/<li.*write tests<\/li>/, result)
11+
assert_match(/data-react-checksum/, result)
12+
end
13+
14+
test '#render accepts strings' do
15+
result = @renderer.render("Todo", {todo: "write more tests"}.to_json)
16+
assert_match(/<li.*write more tests<\/li>/, result)
17+
end
18+
19+
test '#render replays console messages' do
20+
result = @renderer.render("TodoListWithConsoleLog", {todos: ["log some messages"]})
21+
assert_match('console.log.apply(console, ["got initial state"])', result)
22+
assert_match('console.warn.apply(console, ["mounted component"])', result)
23+
assert_match('console.error.apply(console, ["rendered!","foo"])', result)
24+
end
25+
26+
test '#render console messages can be disabled' do
27+
no_log_renderer = React::ServerRendering::SprocketsRenderer.new({replay_console: false})
28+
result = no_log_renderer.render("TodoListWithConsoleLog", {todos: ["log some messages"]})
29+
assert_no_match('console.log.apply(console, ["got initial state"])', result)
30+
assert_no_match('console.warn.apply(console, ["mounted component"])', result)
31+
assert_no_match('console.error.apply(console, ["rendered!","foo"])', result)
32+
end
33+
34+
test '#render errors include stack traces' do
35+
err = assert_raises React::ServerRendering::SprocketsRenderer::PrerenderError do
36+
@renderer.render("NonExistentComponent", {})
37+
end
38+
assert_match("ReferenceError", err.to_s)
39+
assert_match("NonExistentComponent", err.to_s, "it names the component")
40+
assert_match(/\n/, err.to_s, "it includes the multi-line backtrace")
41+
end
42+
43+
test '.new accepts any filenames' do
44+
limited_renderer = React::ServerRendering::SprocketsRenderer.new(files: ["react.js", "components/Todo.js"])
45+
assert_match("get a real job</li>", limited_renderer.render("Todo", {todo: "get a real job"}))
46+
err = assert_raises React::ServerRendering::SprocketsRenderer::PrerenderError do
47+
limited_renderer.render("TodoList", {todos: []})
48+
end
49+
assert_match("ReferenceError", err.to_s, "it doesnt load other files")
1250
end
1351
end

0 commit comments

Comments
 (0)