Skip to content

Commit 207149f

Browse files
author
Robert Mosolgo
committed
Merge pull request #299 from rmosolgo/execjs-renderer
feat(ExecJSRenderer) add ExecJSRenderer
2 parents 614f7e1 + cdaaa59 commit 207149f

File tree

5 files changed

+134
-31
lines changed

5 files changed

+134
-31
lines changed

lib/react/server_rendering.rb

+9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'connection_pool'
2+
require 'react/server_rendering/exec_js_renderer'
23
require 'react/server_rendering/sprockets_renderer'
34

45
module React
@@ -20,5 +21,13 @@ def self.render(component_name, props, prerender_options)
2021
def self.create_renderer
2122
renderer.new(renderer_options)
2223
end
24+
25+
class PrerenderError < RuntimeError
26+
def initialize(component_name, props, js_message)
27+
message = ["Encountered error \"#{js_message}\" when prerendering #{component_name} with #{props}",
28+
js_message.backtrace.join("\n")].join("\n")
29+
super(message)
30+
end
31+
end
2332
end
2433
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# A bare-bones renderer for React.js + Exec.js
2+
# - No Rails dependency
3+
# - No browser concerns
4+
module React
5+
module ServerRendering
6+
class ExecJSRenderer
7+
def initialize(options={})
8+
js_code = options[:code] || raise("Pass `code:` option to instantiate a JS context!")
9+
@context = ExecJS.compile(GLOBAL_WRAPPER + js_code)
10+
end
11+
12+
def render(component_name, props, prerender_options)
13+
render_function = prerender_options.fetch(:render_function, "renderToString")
14+
js_code = <<-JS
15+
(function () {
16+
#{before_render(component_name, props, prerender_options)}
17+
var result = React.#{render_function}(React.createElement(#{component_name}, #{props}));
18+
#{after_render(component_name, props, prerender_options)}
19+
return result;
20+
})()
21+
JS
22+
@context.eval(js_code).html_safe
23+
rescue ExecJS::ProgramError => err
24+
raise React::ServerRendering::PrerenderError.new(component_name, props, err)
25+
end
26+
27+
# Hooks for inserting JS before/after rendering
28+
def before_render(component_name, props, prerender_options); ""; end
29+
def after_render(component_name, props, prerender_options); ""; end
30+
31+
# Handle Node.js & other ExecJS contexts
32+
GLOBAL_WRAPPER = <<-JS
33+
var global = global || this;
34+
var self = self || this;
35+
var window = window || this;
36+
JS
37+
38+
end
39+
end
40+
end
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1+
# Extends ExecJSRenderer for the Rails environment
2+
# - builds JS code out of the asset pipeline
3+
# - stringifies props
4+
# - implements console replay
15
module React
26
module ServerRendering
3-
class SprocketsRenderer
7+
class SprocketsRenderer < ExecJSRenderer
48
def initialize(options={})
59
@replay_console = options.fetch(:replay_console, true)
6-
710
filenames = options.fetch(:files, ["react.js", "components.js"])
8-
js_code = GLOBAL_WRAPPER + CONSOLE_POLYFILL
11+
js_code = CONSOLE_POLYFILL.dup
912

1013
filenames.each do |filename|
1114
js_code << ::Rails.application.assets[filename].to_s
1215
end
1316

14-
@context = ExecJS.compile(js_code)
17+
super(options.merge(code: js_code))
1518
end
1619

1720
def render(component_name, props, prerender_options)
@@ -26,25 +29,12 @@ def render(component_name, props, prerender_options)
2629
props = props.to_json
2730
end
2831

29-
js_code = <<-JS
30-
(function () {
31-
var result = React.#{react_render_method}(React.createElement(#{component_name}, #{props}));
32-
#{@replay_console ? CONSOLE_REPLAY : ""}
33-
return result;
34-
})()
35-
JS
36-
37-
@context.eval(js_code).html_safe
38-
rescue ExecJS::ProgramError => err
39-
raise PrerenderError.new(component_name, props, err)
32+
super(component_name, props, {render_function: react_render_method})
4033
end
4134

42-
# Handle node.js & other RubyRacer contexts
43-
GLOBAL_WRAPPER = <<-JS
44-
var global = global || this;
45-
var self = self || this;
46-
var window = window || this;
47-
JS
35+
def after_render(component_name, props, prerender_options)
36+
@replay_console ? CONSOLE_REPLAY : ""
37+
end
4838

4939
# Reimplement console methods for replaying on the client
5040
CONSOLE_POLYFILL = <<-JS
@@ -68,14 +58,6 @@ def render(component_name, props, prerender_options)
6858
}
6959
})(console.history);
7060
JS
71-
72-
class PrerenderError < RuntimeError
73-
def initialize(component_name, props, js_message)
74-
message = ["Encountered error \"#{js_message}\" when prerendering #{component_name} with #{props}",
75-
js_message.backtrace.join("\n")].join("\n")
76-
super(message)
77-
end
78-
end
7961
end
8062
end
8163
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
require 'test_helper'
2+
3+
DUMMY_IMPLEMENTATION = "
4+
var Todo = null
5+
var React = {
6+
renderToString: function() {
7+
return 'renderToString was called'
8+
},
9+
createElement: function() {}
10+
}
11+
"
12+
13+
class ExecJSRendererTest < ActiveSupport::TestCase
14+
setup do
15+
react_source = Rails.application.assets["react.js"].to_s
16+
todo_component_source = Rails.application.assets["components/Todo.js"].to_s
17+
@renderer = React::ServerRendering::ExecJSRenderer.new(code: react_source + todo_component_source)
18+
end
19+
20+
test '#render returns HTML' do
21+
result = @renderer.render("Todo", {todo: "write tests"}.to_json, {})
22+
assert_match(/<li.*write tests<\/li>/, result)
23+
assert_match(/data-react-checksum/, result)
24+
end
25+
26+
test '#render accepts render_function:' do
27+
result = @renderer.render("Todo", {todo: "write more tests"}.to_json, render_function: "renderToStaticMarkup")
28+
assert_match(/<li>write more tests<\/li>/, result)
29+
assert_no_match(/data-react-checksum/, result)
30+
end
31+
32+
test '#before_render is called before #after_render' do
33+
def @renderer.before_render(name, props, opts)
34+
"throw 'before_render ' + afterRenderVar"
35+
end
36+
37+
def @renderer.after_render(name, props, opts)
38+
"var afterRenderVar = 'assigned_after_render'"
39+
end
40+
41+
error = assert_raises(React::ServerRendering::PrerenderError) do
42+
@renderer.render("Todo", {todo: "write tests"}.to_json, {})
43+
end
44+
45+
assert_match(/before_render/, error.message)
46+
assert_no_match(/assigned_after_render/, error.message)
47+
end
48+
49+
50+
test '#after_render is called after #before_render' do
51+
def @renderer.before_render(name, props, opts)
52+
"var beforeRenderVar = 'assigned_before_render'"
53+
end
54+
55+
def @renderer.after_render(name, props, opts)
56+
"throw 'after_render ' + beforeRenderVar"
57+
end
58+
59+
error = assert_raises(React::ServerRendering::PrerenderError) do
60+
@renderer.render("Todo", {todo: "write tests"}.to_json, {})
61+
end
62+
63+
assert_match(/after_render/, error.message)
64+
assert_match(/assigned_before_render/, error.message)
65+
end
66+
67+
test '.new accepts code:' do
68+
dummy_renderer = React::ServerRendering::ExecJSRenderer.new(code: DUMMY_IMPLEMENTATION)
69+
result = dummy_renderer.render("Todo", {todo: "get a real job"}.to_json, {})
70+
assert_equal("renderToString was called", result)
71+
end
72+
end

test/react/server_rendering/sprockets_renderer_test.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class SprocketsRendererTest < ActiveSupport::TestCase
3838
end
3939

4040
test '#render errors include stack traces' do
41-
err = assert_raises React::ServerRendering::SprocketsRenderer::PrerenderError do
41+
err = assert_raises React::ServerRendering::PrerenderError do
4242
@renderer.render("NonExistentComponent", {}, nil)
4343
end
4444
assert_match(/ReferenceError/, err.to_s)
@@ -49,7 +49,7 @@ class SprocketsRendererTest < ActiveSupport::TestCase
4949
test '.new accepts any filenames' do
5050
limited_renderer = React::ServerRendering::SprocketsRenderer.new(files: ["react.js", "components/Todo.js"])
5151
assert_match(/get a real job<\/li>/, limited_renderer.render("Todo", {todo: "get a real job"}, nil))
52-
err = assert_raises React::ServerRendering::SprocketsRenderer::PrerenderError do
52+
err = assert_raises React::ServerRendering::PrerenderError do
5353
limited_renderer.render("TodoList", {todos: []}, nil)
5454
end
5555
assert_match(/ReferenceError/, err.to_s, "it doesnt load other files")

0 commit comments

Comments
 (0)