From bd6c33c5b9f61b8d1f3bcbf17a0ba75046062152 Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Mon, 8 Jun 2015 10:40:12 -0700 Subject: [PATCH 1/3] feat(ExecJSRenderer) add ExecJSRenderer --- lib/react/server_rendering.rb | 9 +++++ .../server_rendering/exec_js_renderer.rb | 40 +++++++++++++++++++ .../server_rendering/sprockets_renderer.rb | 40 +++++-------------- .../sprockets_renderer_test.rb | 4 +- 4 files changed, 62 insertions(+), 31 deletions(-) create mode 100644 lib/react/server_rendering/exec_js_renderer.rb diff --git a/lib/react/server_rendering.rb b/lib/react/server_rendering.rb index c379be1a..e6774cab 100644 --- a/lib/react/server_rendering.rb +++ b/lib/react/server_rendering.rb @@ -1,4 +1,5 @@ require 'connection_pool' +require 'react/server_rendering/exec_js_renderer' require 'react/server_rendering/sprockets_renderer' module React @@ -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 \ No newline at end of file diff --git a/lib/react/server_rendering/exec_js_renderer.rb b/lib/react/server_rendering/exec_js_renderer.rb new file mode 100644 index 00000000..6315a59a --- /dev/null +++ b/lib/react/server_rendering/exec_js_renderer.rb @@ -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.fetch(: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} + var result = React.#{render_function}(React.createElement(#{component_name}, #{props})); + #{after_render} + return result; + })() + JS + @context.eval(js_code) + rescue ExecJS::ProgramError => err + raise React::ServerRendering::PrerenderError.new(component_name, props, err) + end + + # Hooks for inserting JS before/after rendering + def before_render; ""; end + def after_render; ""; 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 diff --git a/lib/react/server_rendering/sprockets_renderer.rb b/lib/react/server_rendering/sprockets_renderer.rb index 716e2355..082dd7da 100644 --- a/lib/react/server_rendering/sprockets_renderer.rb +++ b/lib/react/server_rendering/sprockets_renderer.rb @@ -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) @@ -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 + @replay_console ? CONSOLE_REPLAY : "" + end # Reimplement console methods for replaying on the client CONSOLE_POLYFILL = <<-JS @@ -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 diff --git a/test/react/server_rendering/sprockets_renderer_test.rb b/test/react/server_rendering/sprockets_renderer_test.rb index 5629fd92..39b67103 100644 --- a/test/react/server_rendering/sprockets_renderer_test.rb +++ b/test/react/server_rendering/sprockets_renderer_test.rb @@ -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) @@ -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") From e8f2da0c2ec91223d53d0caf461f8317bf11f1fa Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Mon, 15 Jun 2015 14:19:33 -0700 Subject: [PATCH 2/3] feat(ExecJSRenderer) pass render args to before_/after_ hooks --- lib/react/server_rendering/exec_js_renderer.rb | 8 ++++---- lib/react/server_rendering/sprockets_renderer.rb | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/react/server_rendering/exec_js_renderer.rb b/lib/react/server_rendering/exec_js_renderer.rb index 6315a59a..049c18ac 100644 --- a/lib/react/server_rendering/exec_js_renderer.rb +++ b/lib/react/server_rendering/exec_js_renderer.rb @@ -13,9 +13,9 @@ def render(component_name, props, prerender_options) render_function = prerender_options.fetch(:render_function, "renderToString") js_code = <<-JS (function () { - #{before_render} + #{before_render(component_name, props, prerender_options)} var result = React.#{render_function}(React.createElement(#{component_name}, #{props})); - #{after_render} + #{after_render(component_name, props, prerender_options)} return result; })() JS @@ -25,8 +25,8 @@ def render(component_name, props, prerender_options) end # Hooks for inserting JS before/after rendering - def before_render; ""; end - def after_render; ""; end + 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 diff --git a/lib/react/server_rendering/sprockets_renderer.rb b/lib/react/server_rendering/sprockets_renderer.rb index 082dd7da..8813d1d6 100644 --- a/lib/react/server_rendering/sprockets_renderer.rb +++ b/lib/react/server_rendering/sprockets_renderer.rb @@ -32,7 +32,7 @@ def render(component_name, props, prerender_options) super(component_name, props, {render_function: react_render_method}) end - def after_render + def after_render(component_name, props, prerender_options) @replay_console ? CONSOLE_REPLAY : "" end From cdaaa59066de7554b6de2261ca1990d37791464d Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Mon, 15 Jun 2015 15:06:17 -0700 Subject: [PATCH 3/3] feat(ExecJSRenderer) test ExecJSRenderer --- .../server_rendering/exec_js_renderer.rb | 4 +- .../server_rendering/exec_js_renderer_test.rb | 72 +++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 test/react/server_rendering/exec_js_renderer_test.rb diff --git a/lib/react/server_rendering/exec_js_renderer.rb b/lib/react/server_rendering/exec_js_renderer.rb index 049c18ac..52ebf2a0 100644 --- a/lib/react/server_rendering/exec_js_renderer.rb +++ b/lib/react/server_rendering/exec_js_renderer.rb @@ -5,7 +5,7 @@ module React module ServerRendering class ExecJSRenderer def initialize(options={}) - js_code = options.fetch(:code) || raise("Pass `code:` option to instantiate a JS context!") + js_code = options[:code] || raise("Pass `code:` option to instantiate a JS context!") @context = ExecJS.compile(GLOBAL_WRAPPER + js_code) end @@ -19,7 +19,7 @@ def render(component_name, props, prerender_options) return result; })() JS - @context.eval(js_code) + @context.eval(js_code).html_safe rescue ExecJS::ProgramError => err raise React::ServerRendering::PrerenderError.new(component_name, props, err) end diff --git a/test/react/server_rendering/exec_js_renderer_test.rb b/test/react/server_rendering/exec_js_renderer_test.rb new file mode 100644 index 00000000..65474140 --- /dev/null +++ b/test/react/server_rendering/exec_js_renderer_test.rb @@ -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(//, 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(/
  • 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 \ No newline at end of file