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.
- checksums.yaml +4 -4
- data/.eslintignore +2 -0
- data/.eslintrc +33 -2
- data/.jscsrc +23 -4
- data/.rubocop.yml +1 -1
- data/README.md +8 -2
- data/app/assets/javascripts/react_on_rails.js +114 -70
- data/app/helpers/react_on_rails_helper.rb +79 -34
- data/docs/additional_reading/{generated_client_code.md → react-and-redux.md} +2 -1
- data/docs/additional_reading/react_router.md +35 -0
- data/docs/coding-style/linters.md +64 -0
- data/docs/coding-style/style.md +42 -0
- data/docs/contributing.md +12 -40
- data/docs/generator_testing_script.md +0 -1
- data/docs/install_and_releasing.md +24 -0
- data/docs/sample_generated_js/README.md +4 -0
- data/docs/sample_generated_js/server-generated.js +23 -9
- data/lib/generators/react_on_rails/base_generator.rb +12 -7
- data/lib/generators/react_on_rails/templates/base/base/client/webpack.client.base.config.js.tt +9 -0
- data/lib/generators/react_on_rails/templates/base/base/client/webpack.client.rails.config.js +0 -7
- data/lib/generators/react_on_rails/templates/base/base/lib/tasks/linters.rake.tt +2 -2
- data/lib/generators/react_on_rails/templates/base/server_rendering/client/webpack.server.rails.config.js +4 -1
- data/lib/generators/react_on_rails/templates/js_linters/client/.eslintrc +34 -3
- data/lib/generators/react_on_rails/templates/js_linters/client/.jscsrc +15 -4
- data/lib/generators/react_on_rails/templates/no_redux/base/client/app/bundles/HelloWorld/components/HelloWorldWidget.jsx +1 -1
- data/lib/generators/react_on_rails/templates/redux/base/client/app/bundles/HelloWorld/reducers/index.jsx +1 -1
- data/lib/generators/react_on_rails/templates/redux/base/client/app/lib/middlewares/loggerMiddleware.js +0 -1
- data/lib/react_on_rails/configuration.rb +4 -2
- data/lib/react_on_rails/prerender_error.rb +31 -0
- data/lib/react_on_rails/server_rendering_pool.rb +9 -18
- data/lib/react_on_rails/version.rb +1 -1
- metadata +9 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 80be69dda56a35cb09284a210eda3b03d30aad07
|
4
|
+
data.tar.gz: 1d2ebaeeb0a8e96017adba8b8193e712154fce01
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 787b0760428d7256c9cbf5475540ec08d56042f9659e3b87601dcfec762ee0f0204eb7ef0f45596245e67827d0b4de27fa68bf9fff12c2a7629baef64c88a9fb
|
7
|
+
data.tar.gz: a5e2b275fcd3690a127d18e7d666b5fa5dc165180793410cc16b45f4ca1ea55c2b44c8de4670b32e273c114e59b5bef2f270d19b2000cf18dc3b99fb2f614abd
|
data/.eslintignore
CHANGED
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": [
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
}
|
data/.rubocop.yml
CHANGED
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`.
|
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
|
73
|
-
domId, trace, generatorFunction);
|
74
|
-
|
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
|
-
|
85
|
-
|
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
|
-
|
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
|
-
|
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
|
189
|
+
function createReactElementOrRouterResult(componentName, props, domId, trace, generatorFunction, location) {
|
160
190
|
if (trace) {
|
161
|
-
console.log('RENDERED ' + componentName + ' with
|
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
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
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
|
-
#{
|
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
|
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
|
-
|
120
|
-
|
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
|
-
|
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,
|
140
|
-
return
|
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
|
-
|
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)
|