Skip to content

Commit 585963f

Browse files
author
Mike Griffith
committed
support new replay_console option that allows captured console.* messages during server-side prerendering to be replayed on client to assist in debugging, per #122
1 parent cb3ca6c commit 585963f

15 files changed

+124
-22
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ Gemfile.lock
33
*.log
44
test/dummy/tmp
55
gemfiles/*.lock
6+
*.swp

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ end
306306

307307
### Server Rendering
308308

309-
For performance and thread-safety reasons, a pool of JS VMs are spun up on application start, and the size of the pool and the timeout on requesting a VM from the pool are configurable. You can also say where you want to grab the `react.js` code from, and if you want to change the filenames for the components (this should be an array of filenames that will be requested from the asset pipeline and concatenated together.)
309+
For performance and thread-safety reasons, a pool of JS VMs are spun up on application start, and the size of the pool and the timeout on requesting a VM from the pool are configurable.
310310

311311
```ruby
312312
# config/environments/application.rb
@@ -316,10 +316,15 @@ MyApp::Application.configure do
316316
config.react.timeout = 20 #seconds
317317
config.react.react_js = lambda {File.read(::Rails.application.assets.resolve('react.js'))}
318318
config.react.component_filenames = ['components.js']
319+
config.react.replay_console = false
319320
end
320-
321321
```
322322

323+
Other configuration options include:
324+
* `react_js`: where you want to grab the javascript library from
325+
* `component_filenames`: an array of filenames that will be requested from the asset pipeline and concatenated together
326+
* `replay_console`: additional debugging by replaying any captured console messages from server-rendering back on the client (note: they will lose their call stack, but it can help point you in right direction)
327+
323328
## CoffeeScript
324329

325330
It is possible to use JSX with CoffeeScript. The caveat is that you will still need to include the docblock. Since CoffeeScript doesn't allow `/* */` style comments, we need to do something a little different. We also need to embed JSX inside backticks so CoffeeScript ignores the syntax it doesn't understand. Here's an example:

lib/react-rails.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'react/jsx'
22
require 'react/renderer'
33
require 'react/rails'
4+
require 'react/console'
45

lib/react/console.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
module React
2+
class Console
3+
def self.polyfill_js
4+
# Overwrite global `console` object with something that can capture messages
5+
# to return to client later for debugging
6+
<<-JS
7+
var console = { history: [] };
8+
['error', 'log', 'info', 'warn'].forEach(function (fn) {
9+
console[fn] = function () {
10+
console.history.push({level: fn, arguments: Array.prototype.slice.call(arguments)});
11+
};
12+
});
13+
JS
14+
end
15+
16+
def self.replay_as_script_js
17+
<<-JS
18+
(function (history) {
19+
if (history && history.length > 0) {
20+
result += '\\n<scr'+'ipt>';
21+
history.forEach(function (msg) {
22+
result += '\\nconsole.' + msg.level + '.apply(console, ' + JSON.stringify(msg.arguments) + ');';
23+
});
24+
result += '\\n</scr'+'ipt>';
25+
}
26+
})(console.history);
27+
JS
28+
end
29+
end
30+
end

lib/react/rails/railtie.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class Railtie < ::Rails::Railtie
7171

7272
do_setup = lambda do
7373
cfg = app.config.react
74-
React::Renderer.setup!( cfg.react_js, cfg.components_js,
74+
React::Renderer.setup!( cfg.react_js, cfg.components_js, cfg.replay_console,
7575
{:size => cfg.max_renderers, :timeout => cfg.timeout})
7676
end
7777

lib/react/renderer.rb

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ def initialize(component_name, props, js_message)
1212

1313
cattr_accessor :pool
1414

15-
def self.setup!(react_js, components_js, args={})
15+
def self.setup!(react_js, components_js, replay_console, args={})
1616
args.assert_valid_keys(:size, :timeout)
1717
@@react_js = react_js
1818
@@components_js = components_js
19+
@@replay_console = replay_console
1920
@@pool.shutdown{} if @@pool
2021
reset_combined_js!
2122
default_pool_options = {:size =>10, :timeout => 20}
@@ -43,9 +44,11 @@ def context
4344
def render(component, args={})
4445
react_props = React::Renderer.react_props(args)
4546
jscode = <<-JS
46-
function() {
47-
return React.renderToString(React.createElement(#{component}, #{react_props}));
48-
}()
47+
(function () {
48+
var result = React.renderToString(React.createElement(#{component}, #{react_props}));
49+
#{@@replay_console ? React::Console.replay_as_script_js : ''}
50+
return result;
51+
})()
4952
JS
5053
context.eval(jscode).html_safe
5154
rescue ExecJS::ProgramError => e
@@ -56,22 +59,15 @@ def render(component, args={})
5659
private
5760

5861
def self.setup_combined_js
59-
<<-CODE
62+
<<-JS
6063
var global = global || this;
6164
var self = self || this;
6265
var window = window || this;
63-
64-
var console = global.console || {};
65-
['error', 'log', 'info', 'warn'].forEach(function (fn) {
66-
if (!(fn in console)) {
67-
console[fn] = function () {};
68-
}
69-
});
70-
66+
#{React::Console.polyfill_js}
7167
#{@@react_js.call};
7268
React = global.React;
7369
#{@@components_js.call};
74-
CODE
70+
JS
7571
end
7672

7773
def self.reset_combined_js!
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
TodoListWithConsoleLog = React.createClass({
2+
getInitialState: function() {
3+
console.log('got initial state');
4+
return({mounted: "nope"});
5+
},
6+
componentWillMount: function() {
7+
console.warn('mounted component');
8+
this.setState({mounted: 'yep'});
9+
},
10+
render: function() {
11+
var x = 'foo';
12+
console.error('rendered!', x);
13+
return (
14+
<ul>
15+
<li>Console Logged</li>
16+
<li id='status'>{this.state.mounted}</li>
17+
{this.props.todos.map(function(todo, i) {
18+
return (<Todo key={i} todo={todo} />)
19+
})}
20+
</ul>
21+
)
22+
}
23+
})
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
Component = React.createClass
22
render: ->
33
`<ExampleComponent videos={this.props.videos} />`
4+
5+
window.Component = Component

test/dummy/app/controllers/server_controller.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,23 @@ class ServerController < ApplicationController
22
def show
33
@todos = %w{todo1 todo2 todo3}
44
end
5+
6+
def console_example
7+
hack_replay_console_config true
8+
@todos = %w{todo1 todo2 todo3}
9+
end
10+
11+
def console_example_suppressed
12+
hack_replay_console_config false
13+
@todos = %w{todo1 todo2 todo3}
14+
end
15+
16+
private
17+
def hack_replay_console_config(value)
18+
# Don't do this in your app; just set it how you want it in config/application.rb
19+
cfg = ::Rails.application.config.react
20+
cfg.replay_console = value
21+
React::Renderer.setup!( cfg.react_js, cfg.components_js, cfg.replay_console,
22+
{:size => cfg.max_renderers, :timeout => cfg.timeout})
23+
end
524
end
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<%= react_component "TodoListWithConsoleLog", {todos: @todos}, {prerender: true} %>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<%= react_component "TodoListWithConsoleLog", {todos: @todos}, {prerender: true} %>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<%= react_component "TodoList", {:todos => @todos}, :prerender => true %>
1+
<%= react_component "TodoList", {todos: @todos}, {prerender: true} %>

test/dummy/config/routes.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
Dummy::Application.routes.draw do
2-
resources :pages, :only => [:show]
3-
resources :server, :only => [:show]
2+
resources :pages, only: [:show]
3+
resources :server, only: [:show] do
4+
collection do
5+
get :console_example
6+
get :console_example_suppressed
7+
end
8+
end
49
end

test/jsxtransform_test.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
}
1717
});
1818
19+
window.Component = Component;
1920
}).call(this);
2021
eos
2122

test/server_rendered_html_test.rb

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,39 @@ def wait_to_ensure_asset_pipeline_detects_changes
1616

1717
FileUtils.cp app_file, file_without_updates
1818
FileUtils.touch app_file
19+
wait_to_ensure_asset_pipeline_detects_changes
1920

2021
begin
2122
get '/server/1'
2223
refute_match(/Updated/, response.body)
2324

24-
wait_to_ensure_asset_pipeline_detects_changes
2525
FileUtils.cp file_with_updates, app_file
2626
FileUtils.touch app_file
27+
wait_to_ensure_asset_pipeline_detects_changes
2728

2829
get '/server/1'
2930
assert_match(/Updated/, response.body)
3031
ensure
3132
# if we have a test failure, we want to make sure that we revert the dummy file
32-
wait_to_ensure_asset_pipeline_detects_changes
3333
FileUtils.mv file_without_updates, app_file
3434
FileUtils.touch app_file
35+
wait_to_ensure_asset_pipeline_detects_changes
3536
end
3637
end
38+
39+
test 'react server rendering shows console output as html comment' do
40+
# Make sure console messages are replayed when requested
41+
get '/server/console_example'
42+
assert_match(/Console Logged/, response.body)
43+
assert_match(/console.log.apply\(console, \["got initial state"\]\)/, response.body)
44+
assert_match(/console.warn.apply\(console, \["mounted component"\]\)/, response.body)
45+
assert_match(/console.error.apply\(console, \["rendered!","foo"\]\)/, response.body)
46+
47+
# Make sure they're not when we don't ask for them
48+
get '/server/console_example_suppressed'
49+
assert_match('Console Logged', response.body)
50+
assert_no_match(/console.log/, response.body)
51+
assert_no_match(/console.warn/, response.body)
52+
assert_no_match(/console.error/, response.body)
53+
end
3754
end

0 commit comments

Comments
 (0)