# Kudos to react-rails for how to do the polyfill of the console!
# https://github.com/reactjs/react-rails/blob/master/lib/react/server_rendering/sprockets_renderer.rb
module ReactOnRails
class ReactRenderer
# Reimplement console methods for replaying on the client
CONSOLE_POLYFILL = <<-JS
var console = { history: [] };
['error', 'log', 'info', 'warn'].forEach(function (level) {
console[level] = function () {
var argArray = Array.prototype.slice.call(arguments);
if (argArray.length > 0) {
argArray[0] = '[SERVER] ' + argArray[0];
}
console.history.push({level: level, arguments: argArray});
};
});
JS
# Script to write to the browser console.
# NOTE: result comes from enclosing closure and is the server generated HTML
# that we intend to write to the browser. Thus, the script tag will get executed right after
# the HTML is rendered.
CONSOLE_REPLAY = <<-JS
var history = console.history;
if (history && history.length > 0) {
consoleReplay += '\\n';
}
JS
DEBUGGER = <<-JS
if (typeof window !== 'undefined') { debugger; }
JS
def initialize(options)
@replay_console = options.fetch(:replay_console) { ReactOnRails.configuration.replay_console }
end
# js_code: JavaScript expression that returns a string.
# Returns an Array:
# [0]: string of HTML for direct insertion on the page by evaluating js_code
# [1]: console messages
# Note, js_code does not have to be based on React.
# Calling code will probably call 'html_safe' on return value before rendering to the view.
def render_js(js_code, options = {})
component_name = options.fetch(:react_component_name, "")
server_side = options.fetch(:server_side, false)
result_js_code = " htmlResult = #{js_code}"
js_code_wrapper = <<-JS
(function () {
var htmlResult = '';
var consoleReplay = '';
#{ReactOnRails::ReactRenderer.wrap_code_with_exception_handler(result_js_code, component_name)}
#{console_replay_js_code}
return JSON.stringify([htmlResult, consoleReplay]);
})()
JS
if ENV["TRACE_REACT_ON_RAILS"].present? # Set to anything to print generated code.
puts "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"
puts "react_renderer.rb: 92"
puts "wrote file tmp/server-generated.js"
File.write("tmp/server-generated.js", js_code_wrapper)
puts "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"
end
if js_context
json_string = js_context.eval(js_code_wrapper)
else
json_string = ExecJS.eval(js_code_wrapper)
end
# element 0 is the html, element 1 is the script tag for the server console output
result = JSON.parse(json_string)
if ReactOnRails.configuration.logging_on_server
console_script = result[1]
console_script_lines = console_script.split("\n")
console_script_lines = console_script_lines[2..-2]
re = /console\.log\.apply\(console, \["\[SERVER\] (?.*)"\]\);/
if console_script_lines
console_script_lines.each do |line|
match = re.match(line)
if match
Rails.logger.info { "[react_on_rails] #{match[:msg]}" }
end
end
end
end
return result
end
def self.wrap_code_with_exception_handler(js_code, component_name)
<<-JS
try {
#{js_code}
}
catch(e) {
var lineOne =
'ERROR: You specified the option generator_function (could be in your defaults) to be\\n';
var lastLine =
'A generator function takes a single arg of props and returns a ReactElement.';
var msg = '';
var shouldBeGeneratorError = lineOne +
'false, but the React component \\'#{component_name}\\' seems to be a generator function.\\n' +
lastLine;
var reMatchShouldBeGeneratorError = /Can't add property context, object is not extensible/;
if (reMatchShouldBeGeneratorError.test(e.message)) {
msg += shouldBeGeneratorError + '\\n\\n';
console.error(shouldBeGeneratorError);
}
var shouldBeGeneratorError = lineOne +
'true, but the React component \\'#{component_name}\\' is not a generator function.\\n' +
lastLine;
var reMatchShouldNotBeGeneratorError = /Cannot call a class as a function/;
if (reMatchShouldNotBeGeneratorError.test(e.message)) {
msg += shouldBeGeneratorError + '\\n\\n';
console.error(shouldBeGeneratorError);
}
#{render_error_messages}
}
JS
end
private
def self.render_error_messages
<<-JS
console.error('Exception in rendering!');
if (e.fileName) {
console.error('location: ' + e.fileName + ':' + e.lineNumber);
}
console.error('message: ' + e.message);
console.error('stack: ' + e.stack);
msg += 'Exception in rendering!\\n' +
(e.fileName ? '\\nlocation: ' + e.fileName + ':' + e.lineNumber : '') +
'\\nMessage: ' + e.message + '\\n\\n' + e.stack;
var reactElement = React.createElement('pre', null, msg);
result = React.renderToString(reactElement);
JS
end
def console_replay_js_code
(@replay_console || ReactOnRails.configuration.logging_on_server) ? CONSOLE_REPLAY : ""
end
def base_js_code(bundle_js_code)
<<-JS
#{CONSOLE_POLYFILL}
#{bundle_js_code};
JS
end
def js_context
if @js_context.nil?
@js_context = begin
server_js_file = ReactOnRails.configuration.server_bundle_js_file
if server_js_file.present? && File.exist?(server_js_file)
bundle_js_code = File.read(server_js_file)
ExecJS.compile(base_js_code(bundle_js_code))
else
if server_js_file.present?
Rails.logger.warn("You specified server rendering JS file: #{server_js_file}, but it cannot be read.")
end
false # using false so we don't try every time if no server_js file
end
end
end
@js_context
end
end
end