react_on_rails 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintignore +2 -0
  3. data/.eslintrc +33 -2
  4. data/.jscsrc +23 -4
  5. data/.rubocop.yml +1 -1
  6. data/README.md +8 -2
  7. data/app/assets/javascripts/react_on_rails.js +114 -70
  8. data/app/helpers/react_on_rails_helper.rb +79 -34
  9. data/docs/additional_reading/{generated_client_code.md → react-and-redux.md} +2 -1
  10. data/docs/additional_reading/react_router.md +35 -0
  11. data/docs/coding-style/linters.md +64 -0
  12. data/docs/coding-style/style.md +42 -0
  13. data/docs/contributing.md +12 -40
  14. data/docs/generator_testing_script.md +0 -1
  15. data/docs/install_and_releasing.md +24 -0
  16. data/docs/sample_generated_js/README.md +4 -0
  17. data/docs/sample_generated_js/server-generated.js +23 -9
  18. data/lib/generators/react_on_rails/base_generator.rb +12 -7
  19. data/lib/generators/react_on_rails/templates/base/base/client/webpack.client.base.config.js.tt +9 -0
  20. data/lib/generators/react_on_rails/templates/base/base/client/webpack.client.rails.config.js +0 -7
  21. data/lib/generators/react_on_rails/templates/base/base/lib/tasks/linters.rake.tt +2 -2
  22. data/lib/generators/react_on_rails/templates/base/server_rendering/client/webpack.server.rails.config.js +4 -1
  23. data/lib/generators/react_on_rails/templates/js_linters/client/.eslintrc +34 -3
  24. data/lib/generators/react_on_rails/templates/js_linters/client/.jscsrc +15 -4
  25. data/lib/generators/react_on_rails/templates/no_redux/base/client/app/bundles/HelloWorld/components/HelloWorldWidget.jsx +1 -1
  26. data/lib/generators/react_on_rails/templates/redux/base/client/app/bundles/HelloWorld/reducers/index.jsx +1 -1
  27. data/lib/generators/react_on_rails/templates/redux/base/client/app/lib/middlewares/loggerMiddleware.js +0 -1
  28. data/lib/react_on_rails/configuration.rb +4 -2
  29. data/lib/react_on_rails/prerender_error.rb +31 -0
  30. data/lib/react_on_rails/server_rendering_pool.rb +9 -18
  31. data/lib/react_on_rails/version.rb +1 -1
  32. metadata +9 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 984a06acb85284e918de174680caf56447f1bbdf
4
- data.tar.gz: 36ac020e260f26fa8b61ec053ada82c362633de6
3
+ metadata.gz: 80be69dda56a35cb09284a210eda3b03d30aad07
4
+ data.tar.gz: 1d2ebaeeb0a8e96017adba8b8193e712154fce01
5
5
  SHA512:
6
- metadata.gz: 5093d94e30d8a8a049127ee0da9f83c79197c89738447663a377bbb6cfd81719b75fa2433d5e4cd63214fcc294b17986e8dcd3f4ecaf691331206fd4038e332b
7
- data.tar.gz: 8adc548734d303cc2776f1e5069b6d72c38c0fcef45cc06f502d6e41696496984260c68543162ce7a0793216615354c525df181a78979fb7ba5fe2d604e97b23
6
+ metadata.gz: 787b0760428d7256c9cbf5475540ec08d56042f9659e3b87601dcfec762ee0f0204eb7ef0f45596245e67827d0b4de27fa68bf9fff12c2a7629baef64c88a9fb
7
+ data.tar.gz: a5e2b275fcd3690a127d18e7d666b5fa5dc165180793410cc16b45f4ca1ea55c2b44c8de4670b32e273c114e59b5bef2f270d19b2000cf18dc3b99fb2f614abd
@@ -1,3 +1,5 @@
1
1
  node_modules
2
2
  coverage
3
3
  spec/react_on_rails/dummy-for-generators
4
+ spec/dummy
5
+ spec/dummy-react-013
data/.eslintrc CHANGED
@@ -6,12 +6,43 @@ extends: eslint-config-airbnb
6
6
  plugins:
7
7
  - react
8
8
 
9
+ globals:
10
+ __DEBUG_SERVER_ERRORS__: true
11
+ __SERVER_ERRORS__: true
12
+
9
13
  env:
10
14
  browser: true
11
15
  node: true
16
+ mocha: true
12
17
 
13
18
  rules:
19
+ ### Variables
20
+ no-undef: 2
21
+ no-unused-vars: [2, { vars: all, args: none }]
22
+
23
+ ### Stylistic issues
14
24
  indent: [1, 2, { SwitchCase: 1, VariableDeclarator: 2 }]
15
- react/sort-comp: 0
16
- react/jsx-quotes: 0
17
25
  id-length: [1, { min: 2, exceptions: [_, e, i, k, v] }]
26
+
27
+ ### React
28
+ jsx-quotes: [1, prefer-double]
29
+ react/display-name: 0
30
+ react/jsx-boolean-value: [1, always]
31
+ react/jsx-curly-spacing: [1, never]
32
+ react/jsx-no-duplicate-props: [2, { ignoreCase: true }]
33
+ react/jsx-no-undef: 2
34
+ react/jsx-sort-prop-types: 0
35
+ react/jsx-sort-props: 0
36
+ react/jsx-uses-react: 2
37
+ react/jsx-uses-vars: 2
38
+ react/no-danger: 0
39
+ react/no-did-mount-set-state: 1
40
+ react/no-did-update-set-state: 0
41
+ react/no-multi-comp: 2
42
+ react/no-unknown-property: 2
43
+ react/prop-types: 1
44
+ react/react-in-jsx-scope: 2
45
+ react/require-extension: [1, { extensions: [.js, .jsx] }]
46
+ react/self-closing-comp: 2
47
+ react/sort-comp: 0 # Should be 1. `statics` should be on top.
48
+ react/wrap-multilines: 2
data/.jscsrc CHANGED
@@ -1,7 +1,26 @@
1
1
  {
2
2
  "preset": "airbnb",
3
- "fileExtensions": [".js", ".jsx"],
4
- "excludeFiles": ["**/build/**", "**/node_modules/**", "**/generated/**", "**/docs/**", "**/tmp/**", "**/sample_generated/**", "**/coverage/**", "**/vendor/**", "**/dummy-for-generators/**"],
5
- "requireTrailingComma": { "ignoreSingleValue": true, "ignoreSingleLine": true },
6
- "validateQuoteMarks": false
3
+ "fileExtensions": [
4
+ ".js",
5
+ ".jsx"
6
+ ],
7
+ "excludeFiles": [
8
+ "**/build/**",
9
+ "**/node_modules/**",
10
+ "**/generated/**",
11
+ "**/docs/**",
12
+ "**/tmp/**",
13
+ "**/sample_generated/**",
14
+ "**/coverage/**",
15
+ "**/vendor/**",
16
+ "**/dummy-for-generators/**",
17
+ "**/dummy/**",
18
+ "**/dummy-react-013/**"
19
+ ],
20
+ "esprima": "babel-jscs",
21
+ "validateQuoteMarks": {
22
+ "mark": "'",
23
+ "escape": true,
24
+ "ignoreJSX": true
25
+ }
7
26
  }
@@ -58,7 +58,7 @@ Metrics/MethodLength:
58
58
  # Offense count: 1
59
59
  # Configuration parameters: CountComments.
60
60
  Metrics/ModuleLength:
61
- Max: 110
61
+ Max: 150
62
62
 
63
63
  # Offense count: 3
64
64
  # Configuration parameters: AllowedVariables.
data/README.md CHANGED
@@ -41,6 +41,8 @@ Please see [Getting Started](#getting-started) for how to set up your Rails proj
41
41
  - [Building the Bundles](#building-the-bundles)
42
42
  - [Globally Exposing Your React Components](#globally-exposing-your-react-components)
43
43
  - [Rails View Helpers In-Depth](#rails-view-helpers-in-depth)
44
+ - [Redux](#redux)
45
+ - [React-Router](#react-router)
44
46
  + [Generator](#generator)
45
47
  - [Understanding the Organization of the Generated Client Code](#understanding-the-organization-of-the-generated-client-code)
46
48
  - [Redux](#redux)
@@ -94,7 +96,6 @@ We're definitely not doing that. With react_on_rails, webpack is mainly generati
94
96
 
95
97
  ```ruby
96
98
  gem "react_on_rails"
97
- gem "therubyracer"
98
99
  ```
99
100
 
100
101
  2. Run the generator with a simple "Hello World" example:
@@ -123,7 +124,9 @@ The generator installs your webpack files in the `client` folder. Foreman uses w
123
124
  Inside your Rails views, you can now use the `react_component` helper method provided by React on Rails.
124
125
 
125
126
  ### Client-Side Rendering vs. Server-Side Rendering
126
- In most cases, you should use the `prerender: false` (default behavior) with the provided helper method to render the React component from your Rails views. In some cases, such as when SEO is vital or many users will not have JavaScript enabled, you can enable server-rendering by passing `prerender: true` to your helper, or you can simply change the default in `config/initializers/react_on_rails`. Your JavaScript can then be first rendered on the server and passed to the client as HTML.
127
+ In most cases, you should use the `prerender: false` (default behavior) with the provided helper method to render the React component from your Rails views. In some cases, such as when SEO is vital or many users will not have JavaScript enabled, you can enable server-rendering by passing `prerender: true` to your helper, or you can simply change the default in `config/initializers/react_on_rails`.
128
+
129
+ Now the server will interpret your JavaScript using [ExecJS](https://github.com/rails/execjs) and pass the resulting HTML to the client. We recommend using [therubyracer](https://github.com/cowboyd/therubyracer) as ExecJS's runtime. The generator will automatically add it to your Gemfile for you.
127
130
 
128
131
  Note that **server-rendering requires globally exposing your components by setting them to `global`, not `window`** (as is the case with client-rendering). If using the generator, you can pass the `--server-rendering` option to configure your application for server-side rendering.
129
132
 
@@ -181,8 +184,10 @@ This is how you actually render the React components you exposed to `window` ins
181
184
  + **options:**
182
185
  + **generator_function:** default is false, set to true if you want to use a generator function rather than a React Component. Why would you do this? For example, you may want the ability to use the passed-in props to initialize a redux store or setup react-router. Or you may want to return different components depending on what's in the props.
183
186
  + **prerender:** enable server-side rendering of component. Set to false when debugging!
187
+ + **router_redirect_callback:** Use this option if you want to provide a custom handler for redirects on server rendering. If you don't specify this, we'll simply change the rendered output to a script that sets window.location to the new route. Set this up exactly like a generator_function. Your function will will take one parameter, containing all the values that react-router gives on a redirect request, such as pathname, search, etc.
184
188
  + **trace:** set to true to print additional debugging information in the browser. Defaults to true for development, off otherwise.
185
189
  + **replay_console:** Default is true. False will disable echoing server-rendering logs to the browser. While this can make troubleshooting server rendering difficult, so long as you have the default configuration of logging_on_server set to true, you'll still see the errors on the server.
190
+ + **raise_on_prerender_error:** Default is false. True will throw an error on the server side rendering. Your controller will have to handle the error.
186
191
  + Any other options are passed to the content tag, including the id
187
192
 
188
193
  #### server_render_js
@@ -315,6 +320,7 @@ As you add more routes to your front-end application, you will need to make the
315
320
  + [Manual Installation](docs/additional_reading/manual_installation.md)
316
321
  + [Node Dependencies and NPM](docs/additional_reading/node_dependencies_and_npm.md)
317
322
  + [Optional Configuration](docs/additional_reading/optional_configuration.md)
323
+ + [React Router](docs/additional_reading/react_router.md)
318
324
  + [Server Rendering Tips](docs/additional_reading/server_rendering_tips.md)
319
325
  + [Tips](docs/additional_reading/tips.md)
320
326
 
@@ -1,79 +1,43 @@
1
1
  (function() {
2
2
  this.ReactOnRails = {};
3
-
4
- ReactOnRails.clientRenderReactComponent = function(options) {
5
- var componentName = options.componentName;
6
- var domId = options.domId;
7
- var propsVarName = options.propsVarName;
8
- var props = options.props;
9
- var trace = options.trace;
10
- var generatorFunction = options.generatorFunction;
11
- var expectTurboLinks = options.expectTurboLinks;
12
-
13
- this[propsVarName] = props;
14
-
15
- var renderIfDomNodePresent = function() {
16
- try {
17
- var domNode = document.getElementById(domId);
18
- if (domNode) {
19
- var reactElement = createReactElement(componentName, propsVarName, props,
20
- domId, trace, generatorFunction);
21
- provideClientReact().render(reactElement, domNode);
22
- }
23
- }
24
- catch (e) {
25
- ReactOnRails.handleError({
26
- e: e,
27
- componentName: componentName,
28
- serverSide: false,
29
- });
30
- }
31
- };
32
-
33
- var turbolinksInstalled = (typeof Turbolinks !== 'undefined');
34
- if (!expectTurboLinks || (!turbolinksInstalled && expectTurboLinks)) {
35
- if (expectTurboLinks) {
36
- console.warn('WARNING: NO TurboLinks detected in JS, but it is in your Gemfile');
37
- }
38
-
39
- document.addEventListener('DOMContentLoaded', function(event) {
40
- renderIfDomNodePresent();
41
- });
42
- } else {
43
- function onPageChange(event) {
44
- var removePageChangeListener = function() {
45
- document.removeEventListener('page:change', onPageChange);
46
- document.removeEventListener('page:before-unload', removePageChangeListener);
47
- var domNode = document.getElementById(domId);
48
- provideClientReact().unmountComponentAtNode(domNode);
49
- };
50
-
51
- document.addEventListener('page:before-unload', removePageChangeListener);
52
-
53
- renderIfDomNodePresent();
54
- }
55
-
56
- document.addEventListener('page:change', onPageChange);
57
- }
58
- };
3
+ var turbolinksInstalled = (typeof Turbolinks !== 'undefined');
59
4
 
60
5
  ReactOnRails.serverRenderReactComponent = function(options) {
61
6
  var componentName = options.componentName;
62
7
  var domId = options.domId;
63
- var propsVarName = options.propsVarName;
64
8
  var props = options.props;
65
9
  var trace = options.trace;
66
10
  var generatorFunction = options.generatorFunction;
67
-
11
+ var location = options.location;
68
12
  var htmlResult = '';
69
13
  var consoleReplay = '';
14
+ var hasErrors = false;
70
15
 
71
16
  try {
72
- var reactElement = createReactElement(componentName, propsVarName, props,
73
- domId, trace, generatorFunction);
74
- htmlResult = provideServerReact().renderToString(reactElement);
17
+ var reactElementOrRouterResult = createReactElementOrRouterResult(componentName, props,
18
+ domId, trace, generatorFunction, location);
19
+ if (isRouterResult(reactElementOrRouterResult)) {
20
+
21
+ // We let the client side handle any redirect
22
+ // Set hasErrors in case we want to throw a Rails exception
23
+ hasErrors = !!reactElementOrRouterResult.routeError;
24
+ if (hasErrors) {
25
+ console.error('React Router ERROR: ' +
26
+ JSON.stringify(reactElementOrRouterResult.routeError));
27
+ } else {
28
+ if (trace) {
29
+ redirectLocation = reactElementOrRouterResult.redirectLocation;
30
+ redirectPath = redirectLocation.pathname + redirectLocation.search;
31
+ console.log('ROUTER REDIRECT: ' + componentName + ' to dom node with id: ' + domId +
32
+ ', redirect to ' + redirectPath);
33
+ }
34
+ }
35
+ } else {
36
+ htmlResult = provideServerReact().renderToString(reactElementOrRouterResult);
37
+ }
75
38
  }
76
39
  catch (e) {
40
+ hasErrors = true;
77
41
  htmlResult = ReactOnRails.handleError({
78
42
  e: e,
79
43
  componentName: componentName,
@@ -81,8 +45,13 @@
81
45
  });
82
46
  }
83
47
 
84
- consoleReplay = ReactOnRails.buildConsoleReplay();
85
- return JSON.stringify([htmlResult, consoleReplay]);
48
+ consoleReplayScript = ReactOnRails.buildConsoleReplay();
49
+
50
+ return JSON.stringify({
51
+ html: htmlResult,
52
+ consoleReplayScript: consoleReplayScript,
53
+ hasErrors: hasErrors,
54
+ });
86
55
  };
87
56
 
88
57
  // Passing either componentName or jsCode
@@ -139,31 +108,91 @@
139
108
  }
140
109
  };
141
110
 
111
+ ReactOnRails.wrapInScriptTags = function(scriptBody) {
112
+ if (!scriptBody) {
113
+ return '';
114
+ }
115
+
116
+ return '\n<script>' + scriptBody + '\n</script>';
117
+ };
118
+
142
119
  ReactOnRails.buildConsoleReplay = function() {
143
120
  var consoleReplay = '';
144
121
 
145
122
  var history = console.history;
146
123
  if (history && history.length > 0) {
147
- consoleReplay += '\n<script>';
148
124
  history.forEach(function(msg) {
149
125
  consoleReplay += '\nconsole.' + msg.level + '.apply(console, ' +
150
126
  JSON.stringify(msg.arguments) + ');';
151
127
  });
128
+ }
129
+
130
+ return ReactOnRails.wrapInScriptTags(consoleReplay);
131
+ };
152
132
 
153
- consoleReplay += '\n</script>';
133
+ function forEachComponent(fn) {
134
+ var els = document.getElementsByClassName('js-react-on-rails-component');
135
+ for (var i = 0; i < els.length; i++) {
136
+ fn(els[i]);
137
+ };
138
+ }
139
+
140
+ function reactOnRailsPageLoaded() {
141
+ forEachComponent(render);
142
+ }
143
+
144
+ function reactOnRailsPageUnloaded() {
145
+ forEachComponent(unmount);
146
+ }
147
+
148
+ function unmount(el) {
149
+ var domId = el.getAttribute('data-dom-id');
150
+ var domNode = document.getElementById(domId);
151
+ provideClientReact().unmountComponentAtNode(domNode);
152
+ }
153
+
154
+ function render(el) {
155
+ var componentName = el.getAttribute('data-component-name');
156
+ var domId = el.getAttribute('data-dom-id');
157
+ var props = JSON.parse(el.getAttribute('data-props'));
158
+ var trace = JSON.parse(el.getAttribute('data-trace'));
159
+ var generatorFunction = JSON.parse(el.getAttribute('data-generator-function'));
160
+ var expectTurboLinks = JSON.parse(el.getAttribute('data-expect-turbo-links'));
161
+
162
+ if (!turbolinksInstalled && expectTurboLinks) {
163
+ console.warn('WARNING: NO TurboLinks detected in JS, but it is in your Gemfile');
154
164
  }
155
165
 
156
- return consoleReplay;
166
+ try {
167
+ var domNode = document.getElementById(domId);
168
+ if (domNode) {
169
+ var reactElementOrRouterResult = createReactElementOrRouterResult(componentName, props,
170
+ domId, trace, generatorFunction);
171
+ if (isRouterResult(reactElementOrRouterResult)) {
172
+ throw new Error('You returned a server side type of react-router error: ' +
173
+ JSON.stringify(reactElementOrRouterResult) +
174
+ '\nYou should return a React.Component always for the client side entry point.');
175
+ } else {
176
+ provideClientReact().render(reactElementOrRouterResult, domNode);
177
+ }
178
+ }
179
+ }
180
+ catch (e) {
181
+ ReactOnRails.handleError({
182
+ e: e,
183
+ componentName: componentName,
184
+ serverSide: false,
185
+ });
186
+ }
157
187
  };
158
188
 
159
- function createReactElement(componentName, propsVarName, props, domId, trace, generatorFunction) {
189
+ function createReactElementOrRouterResult(componentName, props, domId, trace, generatorFunction, location) {
160
190
  if (trace) {
161
- console.log('RENDERED ' + componentName + ' with data_variable ' +
162
- propsVarName + ' to dom node with id: ' + domId);
191
+ console.log('RENDERED ' + componentName + ' to dom node with id: ' + domId);
163
192
  }
164
193
 
165
194
  if (generatorFunction) {
166
- return this[componentName](props);
195
+ return this[componentName](props, location);
167
196
  } else {
168
197
  return React.createElement(this[componentName], props);
169
198
  }
@@ -184,4 +213,19 @@
184
213
 
185
214
  return ReactDOMServer;
186
215
  }
216
+
217
+ function isRouterResult(reactElementOrRouterResult) {
218
+ return !!(reactElementOrRouterResult.redirectLocation ||
219
+ reactElementOrRouterResult.error);
220
+ }
221
+
222
+ // Install listeners when running on the client.
223
+ if (typeof document !== 'undefined') {
224
+ if (!turbolinksInstalled) {
225
+ document.addEventListener('DOMContentLoaded', reactOnRailsPageLoaded);
226
+ } else {
227
+ document.addEventListener('page:before-unload', reactOnRailsPageUnloaded);
228
+ document.addEventListener('page:change', reactOnRailsPageLoaded);
229
+ }
230
+ }
187
231
  }.call(this));
@@ -2,6 +2,7 @@
2
2
  # For any heredoc JS:
3
3
  # 1. The white spacing in this file matters!
4
4
  # 2. Keep all #{some_var} fully to the left so that all indentation is done evenly in that var
5
+ require "react_on_rails/prerender_error"
5
6
 
6
7
  module ReactOnRailsHelper
7
8
  # react_component_name: can be a React component, created using a ES6 class, or
@@ -19,7 +20,7 @@ module ReactOnRailsHelper
19
20
  # global.MyReactComponentApp = MyReactComponentApp;
20
21
  # See spec/dummy/client/app/startup/serverGlobals.jsx and
21
22
  # spec/dummy/client/app/startup/ClientApp.jsx for examples of this
22
- # props: Ruby Hash which contains the properties to pass to the react object
23
+ # props: Ruby Hash or JSON string which contains the properties to pass to the react object
23
24
  #
24
25
  # options:
25
26
  # generator_function: <true/false> default is false, set to true if you want to use a
@@ -31,6 +32,8 @@ module ReactOnRailsHelper
31
32
  # logs to browser. While this can make troubleshooting server rendering difficult,
32
33
  # so long as you have the default configuration of logging_on_server set to
33
34
  # true, you'll still see the errors on the server.
35
+ # raise_on_prerender_error: <true/false> Default to false. True will raise exception on server
36
+ # if the JS code throws
34
37
  # Any other options are passed to the content tag, including the id.
35
38
  def react_component(component_name, props = {}, options = {})
36
39
  # Create the JavaScript and HTML to allow either client or server rendering of the
@@ -50,35 +53,31 @@ module ReactOnRailsHelper
50
53
 
51
54
  # Setup the page_loaded_js, which is the same regardless of prerendering or not!
52
55
  # The reason is that React is smart about not doing extra work if the server rendering did its job.
53
- data_variable_name = "__#{component_name.camelize(:lower)}Data#{react_component_index}__"
54
56
  turbolinks_loaded = Object.const_defined?(:Turbolinks)
55
- # NOTE: props might include closing script tag that might cause XSS
56
- props_string = sanitized_props_string(props)
57
- page_loaded_js = <<-JS
58
- (function() {
59
- window.#{data_variable_name} = #{props_string};
60
- ReactOnRails.clientRenderReactComponent({
61
- componentName: '#{react_component_name}',
62
- domId: '#{dom_id}',
63
- propsVarName: '#{data_variable_name}',
64
- props: window.#{data_variable_name},
65
- trace: #{trace(options)},
66
- generatorFunction: #{generator_function(options)},
67
- expectTurboLinks: #{turbolinks_loaded}
68
- });
69
- })();
70
- JS
71
57
 
72
- data_from_server_script_tag = javascript_tag(page_loaded_js)
58
+ component_specification_tag =
59
+ content_tag(:div,
60
+ "",
61
+ class: "js-react-on-rails-component",
62
+ style: "display:none",
63
+ data: {
64
+ component_name: react_component_name,
65
+ props: props,
66
+ trace: trace(options),
67
+ generator_function: generator_function(options),
68
+ expect_turbolinks: turbolinks_loaded,
69
+ dom_id: dom_id
70
+ })
73
71
 
74
72
  # Create the HTML rendering part
75
- server_rendered_html, console_script =
76
- server_rendered_react_component_html(options, props_string, react_component_name,
77
- data_variable_name, dom_id)
73
+ result = server_rendered_react_component_html(options, props, react_component_name, dom_id)
74
+
75
+ server_rendered_html = result["html"]
76
+ console_script = result["consoleReplayScript"]
78
77
 
79
78
  content_tag_options = options.except(:generator_function, :prerender, :trace,
80
79
  :replay_console, :id, :react_component_name,
81
- :server_side)
80
+ :server_side, :raise_on_prerender_error)
82
81
  content_tag_options[:id] = dom_id
83
82
 
84
83
  rendered_output = content_tag(:div,
@@ -87,7 +86,7 @@ module ReactOnRailsHelper
87
86
 
88
87
  # IMPORTANT: Ensure that we mark string as html_safe to avoid escaping.
89
88
  <<-HTML.html_safe
90
- #{data_from_server_script_tag}
89
+ #{component_specification_tag}
91
90
  #{rendered_output}
92
91
  #{replay_console(options) ? console_script : ''}
93
92
  HTML
@@ -104,7 +103,8 @@ module ReactOnRailsHelper
104
103
  wrapper_js = <<-JS
105
104
  (function() {
106
105
  var htmlResult = '';
107
- var consoleReplay = '';
106
+ var consoleReplayScript = '';
107
+ var hasErrors = false;
108
108
 
109
109
  try {
110
110
  htmlResult =
@@ -114,17 +114,32 @@ module ReactOnRailsHelper
114
114
  } catch(e) {
115
115
  htmlResult = ReactOnRails.handleError({e: e, componentName: null,
116
116
  jsCode: '#{escape_javascript(js_expression)}', serverSide: true});
117
+ hasErrors = true;
117
118
  }
118
119
 
119
- consoleReplay = ReactOnRails.buildConsoleReplay();
120
- return JSON.stringify([htmlResult, consoleReplay]);
120
+ consoleReplayScript = ReactOnRails.buildConsoleReplay();
121
+
122
+ return JSON.stringify({
123
+ html: htmlResult,
124
+ consoleReplayScript: consoleReplayScript,
125
+ hasErrors: hasErrors
126
+ });
127
+
121
128
  })()
122
129
  JS
123
130
 
124
131
  result = ReactOnRails::ServerRenderingPool.server_render_js_with_console_logging(wrapper_js)
125
132
 
126
133
  # IMPORTANT: To ensure that Rails doesn't auto-escape HTML tags, use the 'raw' method.
127
- raw("#{result[0]}#{replay_console(options) ? result[1] : ''}")
134
+ html = result["html"]
135
+ console_log_script = result["consoleLogScript"]
136
+ raw("#{html}#{replay_console(options) ? console_log_script : ''}")
137
+ rescue ExecJS::ProgramError => err
138
+ # rubocop:disable Style/RaiseArgs
139
+ raise ReactOnRails::PrerenderError.new(component_name: "N/A (server_render_js called)",
140
+ err: err,
141
+ js_code: wrapper_js)
142
+ # rubocop:enable Style/RaiseArgs
128
143
  end
129
144
 
130
145
  private
@@ -136,29 +151,59 @@ module ReactOnRailsHelper
136
151
 
137
152
  # Returns Array [0]: html, [1]: script to console log
138
153
  # NOTE, these are NOT html_safe!
139
- def server_rendered_react_component_html(options, props_string, react_component_name, data_variable_name, dom_id)
140
- return ["", ""] unless prerender(options)
154
+ def server_rendered_react_component_html(options, props, react_component_name, dom_id)
155
+ return { "html" => "", "consoleReplayScript" => "" } unless prerender(options)
156
+
157
+ # On server `location` option is added (`location = request.fullpath`)
158
+ # React Router needs this to match the current route
141
159
 
142
160
  # Make sure that we use up-to-date server-bundle
143
161
  ReactOnRails::ServerRenderingPool.reset_pool_if_server_bundle_was_modified
144
162
 
163
+ # Since this code is not inserted on a web page, we don't need to escape.
164
+ props_string = props.is_a?(String) ? props : props.to_json
165
+
145
166
  wrapper_js = <<-JS
146
167
  (function() {
147
168
  var props = #{props_string};
148
169
  return ReactOnRails.serverRenderReactComponent({
149
170
  componentName: '#{react_component_name}',
150
171
  domId: '#{dom_id}',
151
- propsVarName: '#{data_variable_name}',
152
172
  props: props,
153
173
  trace: #{trace(options)},
154
- generatorFunction: #{generator_function(options)}
174
+ generatorFunction: #{generator_function(options)},
175
+ location: '#{request.fullpath}'
155
176
  });
156
177
  })()
157
178
  JS
158
179
 
159
- ReactOnRails::ServerRenderingPool.server_render_js_with_console_logging(wrapper_js)
180
+ result = ReactOnRails::ServerRenderingPool.server_render_js_with_console_logging(wrapper_js)
181
+
182
+ if result["hasErrors"] && raise_on_prerender_error(options)
183
+ # We caught this exception on our backtrace handler
184
+ # rubocop:disable Style/RaiseArgs
185
+ fail ReactOnRails::PrerenderError.new(component_name: react_component_name,
186
+ # Sanitize as this might be browser logged
187
+ props: sanitized_props_string(props),
188
+ err: nil,
189
+ js_code: wrapper_js,
190
+ console_messages: result["consoleReplayScript"])
191
+ # rubocop:enable Style/RaiseArgs
192
+ end
193
+ result
160
194
  rescue ExecJS::ProgramError => err
161
- raise ReactOnRails::ServerRenderingPool::PrerenderError.new(react_component_name, props_string, err)
195
+ # This error came from execJs
196
+ # rubocop:disable Style/RaiseArgs
197
+ raise ReactOnRails::PrerenderError.new(component_name: react_component_name,
198
+ # Sanitize as this might be browser logged
199
+ props: sanitized_props_string(props),
200
+ err: err,
201
+ js_code: wrapper_js)
202
+ # rubocop:enable Style/RaiseArgs
203
+ end
204
+
205
+ def raise_on_prerender_error(options)
206
+ options.fetch(:raise_on_prerender_error) { ReactOnRails.configuration.raise_on_prerender_error }
162
207
  end
163
208
 
164
209
  def trace(options)