Skip to content

feat(ExecJSRenderer) add ExecJSRenderer #299

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 19, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions lib/react/server_rendering.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'connection_pool'
require 'react/server_rendering/exec_js_renderer'
require 'react/server_rendering/sprockets_renderer'

module React
Expand All @@ -20,5 +21,13 @@ def self.render(component_name, props, prerender_options)
def self.create_renderer
renderer.new(renderer_options)
end

class PrerenderError < RuntimeError
def initialize(component_name, props, js_message)
message = ["Encountered error \"#{js_message}\" when prerendering #{component_name} with #{props}",
js_message.backtrace.join("\n")].join("\n")
super(message)
end
end
end
end
40 changes: 40 additions & 0 deletions lib/react/server_rendering/exec_js_renderer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# A bare-bones renderer for React.js + Exec.js
# - No Rails dependency
# - No browser concerns
module React
module ServerRendering
class ExecJSRenderer
def initialize(options={})
js_code = options[:code] || raise("Pass `code:` option to instantiate a JS context!")
@context = ExecJS.compile(GLOBAL_WRAPPER + js_code)
end

def render(component_name, props, prerender_options)
render_function = prerender_options.fetch(:render_function, "renderToString")
js_code = <<-JS
(function () {
#{before_render(component_name, props, prerender_options)}
var result = React.#{render_function}(React.createElement(#{component_name}, #{props}));
#{after_render(component_name, props, prerender_options)}
return result;
})()
JS
@context.eval(js_code).html_safe
rescue ExecJS::ProgramError => err
raise React::ServerRendering::PrerenderError.new(component_name, props, err)
end

# Hooks for inserting JS before/after rendering
def before_render(component_name, props, prerender_options); ""; end
def after_render(component_name, props, prerender_options); ""; end

# Handle Node.js & other ExecJS contexts
GLOBAL_WRAPPER = <<-JS
var global = global || this;
var self = self || this;
var window = window || this;
JS

end
end
end
40 changes: 11 additions & 29 deletions lib/react/server_rendering/sprockets_renderer.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
# Extends ExecJSRenderer for the Rails environment
# - builds JS code out of the asset pipeline
# - stringifies props
# - implements console replay
module React
module ServerRendering
class SprocketsRenderer
class SprocketsRenderer < ExecJSRenderer
def initialize(options={})
@replay_console = options.fetch(:replay_console, true)

filenames = options.fetch(:files, ["react.js", "components.js"])
js_code = GLOBAL_WRAPPER + CONSOLE_POLYFILL
js_code = CONSOLE_POLYFILL.dup

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

@context = ExecJS.compile(js_code)
super(options.merge(code: js_code))
end

def render(component_name, props, prerender_options)
Expand All @@ -26,25 +29,12 @@ def render(component_name, props, prerender_options)
props = props.to_json
end

js_code = <<-JS
(function () {
var result = React.#{react_render_method}(React.createElement(#{component_name}, #{props}));
#{@replay_console ? CONSOLE_REPLAY : ""}
return result;
})()
JS

@context.eval(js_code).html_safe
rescue ExecJS::ProgramError => err
raise PrerenderError.new(component_name, props, err)
super(component_name, props, {render_function: react_render_method})
end

# Handle node.js & other RubyRacer contexts
GLOBAL_WRAPPER = <<-JS
var global = global || this;
var self = self || this;
var window = window || this;
JS
def after_render(component_name, props, prerender_options)
@replay_console ? CONSOLE_REPLAY : ""
end

# Reimplement console methods for replaying on the client
CONSOLE_POLYFILL = <<-JS
Expand All @@ -68,14 +58,6 @@ def render(component_name, props, prerender_options)
}
})(console.history);
JS

class PrerenderError < RuntimeError
def initialize(component_name, props, js_message)
message = ["Encountered error \"#{js_message}\" when prerendering #{component_name} with #{props}",
js_message.backtrace.join("\n")].join("\n")
super(message)
end
end
end
end
end
72 changes: 72 additions & 0 deletions test/react/server_rendering/exec_js_renderer_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
require 'test_helper'

DUMMY_IMPLEMENTATION = "
var Todo = null
var React = {
renderToString: function() {
return 'renderToString was called'
},
createElement: function() {}
}
"

class ExecJSRendererTest < ActiveSupport::TestCase
setup do
react_source = Rails.application.assets["react.js"].to_s
todo_component_source = Rails.application.assets["components/Todo.js"].to_s
@renderer = React::ServerRendering::ExecJSRenderer.new(code: react_source + todo_component_source)
end

test '#render returns HTML' do
result = @renderer.render("Todo", {todo: "write tests"}.to_json, {})
assert_match(/<li.*write tests<\/li>/, result)
assert_match(/data-react-checksum/, result)
end

test '#render accepts render_function:' do
result = @renderer.render("Todo", {todo: "write more tests"}.to_json, render_function: "renderToStaticMarkup")
assert_match(/<li>write more tests<\/li>/, result)
assert_no_match(/data-react-checksum/, result)
end

test '#before_render is called before #after_render' do
def @renderer.before_render(name, props, opts)
"throw 'before_render ' + afterRenderVar"
end

def @renderer.after_render(name, props, opts)
"var afterRenderVar = 'assigned_after_render'"
end

error = assert_raises(React::ServerRendering::PrerenderError) do
@renderer.render("Todo", {todo: "write tests"}.to_json, {})
end

assert_match(/before_render/, error.message)
assert_no_match(/assigned_after_render/, error.message)
end


test '#after_render is called after #before_render' do
def @renderer.before_render(name, props, opts)
"var beforeRenderVar = 'assigned_before_render'"
end

def @renderer.after_render(name, props, opts)
"throw 'after_render ' + beforeRenderVar"
end

error = assert_raises(React::ServerRendering::PrerenderError) do
@renderer.render("Todo", {todo: "write tests"}.to_json, {})
end

assert_match(/after_render/, error.message)
assert_match(/assigned_before_render/, error.message)
end

test '.new accepts code:' do
dummy_renderer = React::ServerRendering::ExecJSRenderer.new(code: DUMMY_IMPLEMENTATION)
result = dummy_renderer.render("Todo", {todo: "get a real job"}.to_json, {})
assert_equal("renderToString was called", result)
end
end
4 changes: 2 additions & 2 deletions test/react/server_rendering/sprockets_renderer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class SprocketsRendererTest < ActiveSupport::TestCase
end

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