Skip to content

Commit bd6c33c

Browse files
committed
feat(ExecJSRenderer) add ExecJSRenderer
1 parent 144f9ae commit bd6c33c

File tree

4 files changed

+62
-31
lines changed

4 files changed

+62
-31
lines changed

lib/react/server_rendering.rb

Lines changed: 9 additions & 0 deletions
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
Lines changed: 40 additions & 0 deletions
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.fetch(: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}
17+
var result = React.#{render_function}(React.createElement(#{component_name}, #{props}));
18+
#{after_render}
19+
return result;
20+
})()
21+
JS
22+
@context.eval(js_code)
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; ""; end
29+
def after_render; ""; 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
Lines changed: 11 additions & 29 deletions
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
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

test/react/server_rendering/sprockets_renderer_test.rb

Lines changed: 2 additions & 2 deletions
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)