├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .nvmrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples └── movie catalog │ ├── Readme.md │ ├── index.js │ ├── movies.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.js │ ├── routes.jsx │ ├── styles.css │ └── views │ │ ├── 404.jsx │ │ ├── 500.jsx │ │ ├── detail.jsx │ │ ├── layout.jsx │ │ └── list.jsx │ ├── server.js │ └── webpack.config.js ├── index.js ├── lib ├── client.js ├── config.json ├── expressView.js ├── performance.js ├── reactRouterServerErrors.js ├── server.js └── util.js ├── package.json ├── test ├── client.js ├── expressView.js ├── fixtures │ ├── assertions.json │ ├── reactRoutes.jsx │ └── views │ │ ├── account.jsx │ │ ├── app.jsx │ │ ├── layout.jsx │ │ └── profile.jsx └── server.js └── upgrade-guide.md /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | examples/movie catalog/node_modules 2 | examples/movie catalog/public/bundle.js 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .build 3 | .idea 4 | .project 5 | .settings 6 | *.swp 7 | logs 8 | *.log 9 | coverage/ 10 | examples/movie catalog/public/bundle.js 11 | package-lock.json 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples/ 2 | test/ 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 4 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 4 4 | - 6 5 | - 8 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 4.4.0 2 | * Add support to React 16 3 | * Update peerDependencies to remove installation warnings 4 | * Update devDependencies to execute test in React 16 5 | 6 | ## 4.3.0 (Feb 7 2017) 7 | * Add option to change injected script type from the default 'application/json'. 8 | 9 | ## 4.2.1 (Dec 9 2016) 10 | * Converted from escape-html to jsesc (https://github.com/paypal/react-engine/pull/185) 11 | 12 | ## 4.2.0 (Oct 24 2016) 13 | * XSS safe implementation of prop passing from server to client (https://github.com/paypal/react-engine/pull/179/) 14 | * Specify `react` and `react-dom` as peer dependencies (https://github.com/paypal/react-engine/pull/177) 15 | * replace full lodash inlined into client with lodash/assign (https://github.com/paypal/react-engine/pull/176) 16 | * support dot-lookup paths in renderOptionsKeysToFilter (https://github.com/paypal/react-engine/pull/175) 17 | 18 | ## 4.1.0 (Aug 11 2016) 19 | * safeguard against property over look while fusing together data object and routerProps object in the server render (https://github.com/paypal/react-engine/pull/173/) 20 | 21 | ## 4.0.0 (July 1 2016) 22 | * removed `react-dom` from being a `react-engine` dependency 23 | * updated a lot of dependencies and fixed tests 24 | * remove JSCS/grunt and added airbnb eslint config 25 | 26 | ## 3.4.1 (May 16 2016) 27 | * add backward compatibility for react-router@1 (https://github.com/paypal/react-engine/pull/159) 28 | 29 | ## 3.4.0 (May 13 2016) 30 | * Update deprecated `history` and `RoutingContext` for react-router (https://github.com/paypal/react-engine/pull/155) 31 | 32 | ## 3.3.0 (Apr 30 2016) 33 | * Added scriptLocation server option to allow consumers to specify location of REACT_ENGINE script (https://github.com/paypal/react-engine/pull/153) 34 | * Support ES6 module syntax for routes (https://github.com/paypal/react-engine/pull/154) 35 | 36 | ## 3.2.2 (Apr 19 2016) 37 | 38 | * fix #151, make react-router optional (https://github.com/paypal/react-engine/pull/152) 39 | 40 | ## 3.2.1 (Apr 12 2016) 41 | 42 | * Support ES6 module syntax for React views (https://github.com/paypal/react-engine/pull/149) 43 | 44 | ## 3.2.0 (Mar 27 2016) 45 | 46 | * Allow consumers to override history object (https://github.com/paypal/react-engine/issues/126) 47 | 48 | ## 3.1.0 (Jan 25 2016) 49 | 50 | * fix - set implicit extension to import file names 51 | * fix - Allow consumers to override history object 52 | * Use path instead of pathname to ensure querystring is not stripped - https://github.com/paypal/react-engine/pull/131 53 | * Client-side error when using code splitting in webpack - https://github.com/paypal/react-engine/pull/129 54 | 55 | ## 3.0.0 (Jan 10 2016) 56 | 57 | * [v3.x] - support react-router@1 and react@0.14 58 | 59 | ## 2.6.2 (Jan 3 2016) 60 | 61 | * fix - lodash-node package is deprecated(https://github.com/paypal/react-engine/issues/122) 62 | 63 | ## 2.6.1 (Dec 30 2015) 64 | 65 | * fix undefined createOptions var [client.js] (https://github.com/paypal/react-engine/issues/119) 66 | 67 | ## 2.6.0 (Nov 06 2015) 68 | 69 | * make the render root configurable (https://github.com/paypal/react-engine/issues/68) 70 | 71 | ## 2.5.0 (Oct 29 2015) 72 | 73 | * Throw an error only if peer dependency is not installed and is really required (https://github.com/paypal/react-engine/pull/98) 74 | 75 | ## 2.4.0 (Oct 15 2015) 76 | 77 | * Export Router object to consumers. (https://github.com/paypal/react-engine/issues/81) 78 | 79 | ## 2.3.0 (Oct 11 2015) 80 | 81 | * Allow custom doctype option. (https://github.com/paypal/react-engine/pull/96) 82 | 83 | ## 2.2.1 (Oct 09 2015) 84 | 85 | * make the clearRequireCacheInDir platform windows friendly (https://github.com/paypal/react-engine/issues/93) 86 | 87 | ## 2.2.0 (Sep 02 2015) 88 | 89 | * Allow finer grain control of render properties (https://github.com/paypal/react-engine/issues/73) 90 | 91 | ## 2.1.0 (Aug 20 2015) 92 | 93 | * resolve cache clear logic based on the 'view cache' (https://github.com/paypal/react-engine/issues/74) 94 | * updated readme with migration to v2.x notes (https://github.com/paypal/react-engine/issues/75) 95 | * updated readme references of Isomorphic JavaScript to Universal JavaScript (https://github.com/paypal/react-engine/issues/60) 96 | 97 | ## 2.0.0 (Aug 1 2015) 98 | 99 | * Major API changes (specifically the options object property name changes) 100 | * React-Router config properties can be passed through the react engine now. 101 | 102 | ## 1.7.0 (June 22, 2015) 103 | 104 | * Windows path fix (https://github.com/paypal/react-engine/pull/41) 105 | 106 | ## 1.6.0 (May 13, 2015) 107 | 108 | * expose state/data on the client side using additional function called data. Helps in flux implementations, which need data even before booting. 109 | 110 | ## 1.5.0 (May 9, 2015) 111 | 112 | * made peerDependencies and dependencies, `react` and `react-router`'s versions to be more flexible. 113 | 114 | ## 1.4.1 (May 7, 2015) 115 | 116 | * Fix: https://github.com/paypal/react-engine/issues/28 117 | * add unit tests for expressView.js 118 | * change tape test reporter from `tap-spec` to `faucet` 119 | 120 | ## 1.4.0 (May 3, 2015) 121 | 122 | * remove passing react & react-router as options in the client boot. 123 | * lock down version of `jsdom` (latest versions seem to fail tests in Node.js env) 124 | 125 | ## 1.3.0 (April 30, 2015) 126 | 127 | * added performance profiling 128 | 129 | ## 1.2.0 (April 25, 2015) 130 | 131 | * generate semantic html by injecting script tag before end of html tag 132 | * https://github.com/paypal/react-engine/pull/16 133 | 134 | ## 1.1.0 (April 11, 2015) 135 | 136 | * added an API to the client side code to expose data. 137 | * added ChangeLog and .npmignore 138 | * added tap-spec to pretty format tape test results 139 | * added .editorconfig file 140 | 141 | ## 1.0.0 (April 9, 2015) 142 | 143 | * initial release 144 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | 3 | Version 2.0, January 2004 4 | 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 12 | 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or 17 | are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct 18 | or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership 19 | of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 20 | 21 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 22 | 23 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source 24 | code, documentation source, and configuration files. 25 | 26 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including 27 | but not limited to compiled object code, generated documentation, and conversions to other media types. 28 | 29 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as 30 | indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix 31 | below). 32 | 33 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work 34 | and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an 35 | original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain 36 | separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 37 | 38 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or 39 | additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the 40 | Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. 41 | For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent 42 | to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source 43 | code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of 44 | discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in 45 | writing by the copyright owner as "Not a Contribution." 46 | 47 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been 48 | received by Licensor and subsequently incorporated within the Work. 49 | 50 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to 51 | You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, 52 | prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such 53 | Derivative Works in Source or Object form. 54 | 55 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You 56 | a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent 57 | license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license 58 | applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution 59 | (s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You 60 | institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that 61 | the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then 62 | any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is 63 | filed. 64 | 65 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with 66 | or without modifications, and in Source or Object form, provided that You meet the following conditions: 67 | 68 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 69 | You must cause any modified files to carry prominent notices stating that You changed the files; and 70 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and 71 | attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the 72 | Derivative Works; and 73 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute 74 | must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices 75 | that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text 76 | file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the 77 | Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices 78 | normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. 79 | You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to 80 | the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the 81 | License. 82 | 83 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms 84 | and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a 85 | whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in 86 | this License. 87 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for 88 | inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any 89 | additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any 90 | separate license agreement you may have executed with Licensor regarding such Contributions. 91 | 92 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product 93 | names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and 94 | reproducing the content of the NOTICE file. 95 | 96 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and 97 | each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 98 | express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, 99 | MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness 100 | of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this 101 | License. 102 | 103 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, 104 | or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in 105 | writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or 106 | consequential damages of any character arising as a result of this License or out of the use or inability to use the 107 | Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or 108 | any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such 109 | damages. 110 | 111 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may 112 | choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations 113 | and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own 114 | behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, 115 | defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor 116 | by reason of your accepting any such warranty or additional liability. 117 | 118 | END OF TERMS AND CONDITIONS 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-engine 2 | 3 | [![Build Status](https://travis-ci.org/paypal/react-engine.svg?branch=master)](https://travis-ci.org/paypal/react-engine) 4 | 5 | ### What is react-engine? 6 | * a react render engine for [Universal](https://medium.com/@mjackson/universal-javascript-4761051b7ae9) (previously [Isomorphic](http://nerds.airbnb.com/isomorphic-javascript-future-web-apps/)) JavaScript apps written with express 7 | * renders both plain react views and **optionally** react-router views 8 | * enables server rendered views to be client mountable 9 | 10 | 11 | ### Install 12 | ```sh 13 | # In your express app, react-engine needs to be installed alongside react/react-dom (react-router is optional) 14 | $ npm install react-engine react react-dom react-router --save 15 | ``` 16 | 17 | ### Usage On Server Side 18 | ###### Setup in an Express app 19 | ```javascript 20 | var Express = require('express'); 21 | var ReactEngine = require('react-engine'); 22 | 23 | var app = Express(); 24 | 25 | // create an engine instance 26 | var engine = ReactEngine.server.create({ 27 | /* 28 | see the complete server options spec here: 29 | https://github.com/paypal/react-engine#server-options-spec 30 | */ 31 | }); 32 | 33 | // set the engine 34 | app.engine('.jsx', engine); 35 | 36 | // set the view directory 37 | app.set('views', __dirname + '/views'); 38 | 39 | // set jsx or js as the view engine 40 | // (without this you would need to supply the extension to res.render()) 41 | // ex: res.render('index.jsx') instead of just res.render('index'). 42 | app.set('view engine', 'jsx'); 43 | 44 | // finally, set the custom view 45 | app.set('view', require('react-engine/lib/expressView')); 46 | ``` 47 | 48 | ###### Setup in a [KrakenJS](http://krakenjs.com) app's config.json 49 | ```json 50 | { 51 | "express": { 52 | "view engine": "jsx", 53 | "view": "require:react-engine/lib/expressView", 54 | }, 55 | "view engines": { 56 | "jsx": { 57 | "module": "react-engine/lib/server", 58 | "renderer": { 59 | "method": "create", 60 | "arguments": [{ 61 | /* 62 | see the complete server options spec here: 63 | https://github.com/paypal/react-engine#server-options-spec 64 | */ 65 | }] 66 | } 67 | } 68 | } 69 | } 70 | ``` 71 | 72 | ###### Server options spec 73 | Pass in a JavaScript object as options to the react-engine's [server engine create method](#setup-in-an-express-app). 74 | The options object should contain the mandatory `routes` property with the route definition. 75 | 76 | Additionally, it can contain the following **optional** properties, 77 | 78 | - `docType`: \ - a string that can be used as a doctype (_Default: ``_). 79 | (docType might not make sense if you are rendering partials/sub page components, in that case you can pass an empty string as docType) 80 | - `routesFilePath`: \ - path for the file that contains the react router routes. 81 | react-engine uses this behind the scenes to reload the routes file in 82 | cases where [express's app property](http://expressjs.com/api.html#app.set) `view cache` is false, this way you don't need to restart the server every time a change is made in the view files or routes file. 83 | - `renderOptionsKeysToFilter`: \ - an array of keys that need to be filtered out from the data object that gets fed into the react component for rendering. [more info](#data-for-component-rendering) 84 | - `performanceCollector`: \ - to collects [perf stats](#performance-profiling) 85 | - `scriptLocation`: \ - where in the HTML you want the client data (i.e. ``) to be appended (_Default: `body`_). 86 | If the value is undefined or set to `body` the script is placed before the `` tag. 87 | The only other value is `head` which appends the script before the `` tag. 88 | 89 | - `staticMarkup`: \ - a boolean that indicates if render components without React data attributes and client data. (_Default: `false`_). This is useful if you want to render simple static page, as stripping away the extra React attributes and client data can save lots of bytes. 90 | - `scriptType`: \ - a string that can be used as the type for the script (if it is included, which is only if staticMarkup is false). (_Default: `application/json`_). 91 | 92 | ###### Rendering views on server side 93 | ```js 94 | var data = {}; // your data model 95 | 96 | // for a simple react view rendering 97 | res.render(viewName, data); 98 | 99 | // for react-router rendering 100 | // pass in the `url` and react-engine 101 | // will run the react-router behind the scenes. 102 | res.render(req.url, data); 103 | ``` 104 | 105 | ### Usage On Client Side (Mounting) 106 | ```js 107 | // assuming we use a module bundler like `webpack` or `browserify` 108 | var client = require('react-engine/lib/client'); 109 | 110 | // finally, boot whenever your app is ready 111 | // example: 112 | document.addEventListener('DOMContentLoaded', function onLoad() { 113 | 114 | // `onBoot` - Function (optional) 115 | // returns data that was used 116 | // during rendering as the first argument 117 | // the second argument is the `history` object that was created behind the scenes 118 | // (only available while using react-router) 119 | client.boot(/* client options object */, function onBoot(data, history) { 120 | 121 | }); 122 | }; 123 | 124 | // if the data is needed before booting on 125 | // client, call `data` function anytime to get it. 126 | // example: 127 | var data = client.data(); 128 | ``` 129 | 130 | ###### Client options spec 131 | Pass in a JavaScript object as options to the react-engine's client boot function. 132 | It can contain the following properties, 133 | 134 | - `routes` : **required** - _Object_ - the route definition file. 135 | - `viewResolver` : **required** - _Function_ - a function that react-engine needs to resolve the view file. 136 | an example of the viewResolver can be [found here](https://github.com/paypal/react-engine/blob/ecd27b30a9028d3f02b8f8e89d355bb5fc909de9/examples/simple/public/index.js#L29). 137 | - `mountNode` : **optional** - _HTMLDOMNode_ - supply a HTML DOM Node to mount the server rendered component in the case of partial/non-full page rendering. 138 | - `history` : **optional** - _Object_ - supply any custom history object to be used by the react-router. 139 | 140 | ### Data for component rendering 141 | The actual data that gets fed into the component for rendering is the `renderOptions` object that [express generates](https://github.com/strongloop/express/blob/2f8ac6726fa20ab5b4a05c112c886752868ac8ce/lib/application.js#L535-L588). 142 | 143 | If you don't want to pass all that data, you can pass in an array of object keys or dot-lookup paths that react-engine can filter out from the `renderOptions` object before passing it into the component for rendering. 144 | 145 | ```javascript 146 | // example of using `renderOptionsKeysToFilter` to filter `renderOptions` keys 147 | var engine = ReactEngine.server.create({ 148 | renderOptionsKeysToFilter: [ 149 | 'mySensitiveData', 150 | 'somearrayAtIndex[3].deeply.nested' 151 | ], 152 | routes: require(path.join(__dirname + './reactRoutes')) 153 | }); 154 | ``` 155 | 156 | Notes: 157 | - The strings `renderOptionsKeysToFilter` will be used with [lodash.unset](https://lodash.com/docs/#unset), so they can be either plain object keys for first-level properties of `renderOptions`, or dot-separated "lookup paths" as shown in the `lodash.unset` documentation. Use these lookup paths to filter out nested sub-properties. 158 | - By default, the following three keys are always filtered out from `renderOptions` no matter whether `renderOptionsKeysToFilter` is configured or not. 159 | - `settings` 160 | - `enrouten` 161 | - `_locals` 162 | 163 | ### Handling redirects and route not found errors on the server side 164 | While using react-router, it matches the url to a component based on the app's defined routes. react-engine captures the redirects and not-found cases that are encountered while trying to run the react-router's [match function on the server side](https://github.com/reactjs/react-router/blob/master/docs/guides/ServerRendering.md). 165 | 166 | To handle the above during the lifecycle of a request, add an error type check in your express error middleware. The following are the three types of error that get thrown by react-engine: 167 | 168 | Error Type | Description 169 | -------------------- | -------------------------------------------------------- 170 | MATCH_REDIRECT** | indicates that the url matched to a redirection 171 | MATCH_NOT_FOUND | indicates that the url did not match to any component 172 | MATCH_INTERNAL_ERROR | indicates that react-router encountered an internal error 173 | 174 | _** for MATCH_REDIRECT error, `redirectLocation` property of the err has the new redirection location_ 175 | 176 | ```javascript 177 | // example express error middleware 178 | app.use(function(err, req, res, next) { 179 | console.error(err); 180 | 181 | // http://expressjs.com/en/guide/error-handling.html 182 | if (res.headersSent) { 183 | return next(err); 184 | } 185 | 186 | if (err._type && err._type === ReactEngine.reactRouterServerErrors.MATCH_REDIRECT) { 187 | return res.redirect(302, err.redirectLocation); 188 | } 189 | else if (err._type && err._type === ReactEngine.reactRouterServerErrors.MATCH_NOT_FOUND) { 190 | return res.status(404).send('Route Not Found!'); 191 | } 192 | else { 193 | // for ReactEngine.reactRouterServerErrors.MATCH_INTERNAL_ERROR or 194 | // any other error we just send the error message back 195 | return res.status(500).send(err.message); 196 | } 197 | }); 198 | ``` 199 | 200 | ### Yeoman Generator 201 | There is a Yeoman generator available to create a new express or KrakenJS application which uses react-engine: 202 | [generator-react-engine](https://www.npmjs.com/package/generator-react-engine). 203 | 204 | ### Performance Profiling 205 | 206 | Pass in a function to the `performanceCollector` property to collect the `stats` 207 | object for every render. 208 | 209 | ##### `stats` 210 | The object that contains the stats info for each render by react-engine. 211 | It has the below properties. 212 | - `name` - Name of the template or the url in case of react router rendering. 213 | - `startTime` - The start time of render. 214 | - `endTime` - The completion time of render. 215 | - `duration` - The duration taken to render (in milliseconds). 216 | 217 | ```js 218 | // example 219 | function collector(stats) { 220 | console.log(stats); 221 | } 222 | 223 | var engine = require('react-engine').server.create({ 224 | routes: './routes.jsx' 225 | performanceCollector: collector 226 | }); 227 | ``` 228 | 229 | ### Notes 230 | * On the client side, the state is exposed in a script tag whose id is `react-engine-props` 231 | * When Express's `view cache` app property is false (mostly in non-production environments), views are automatically reloaded before render. So there is no need to restart the server for seeing the changes. 232 | * You can use `js` as the engine if you decide not to write your react views in `jsx`. 233 | * [Blog on react-engine](https://www.paypal-engineering.com/2015/04/27/isomorphic-react-apps-with-react-engine/) 234 | * You can add [nonce](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#Unsafe_inline_script) in `_locals`, which will be added in `script` tag that gets injected into the server rendered pages, like `res.locals.nonce = 'nonce value'` 235 | 236 | 237 | ### License 238 | [Apache Software License v2.0](http://www.apache.org/licenses/LICENSE-2.0) 239 | -------------------------------------------------------------------------------- /examples/movie catalog/Readme.md: -------------------------------------------------------------------------------- 1 | # react-engine example app 2 | This movie catalog app illustrates the usage of react-engine to build and run an universal/isomorphic app. 3 | 4 | ## app composition 5 | * [express - 4.x](https://github.com/strongloop/express) on the server side 6 | * [react-engine - 4.x](https://github.com/paypal/react-engine) as the express view render engine 7 | * [react - 16.x](https://github.com/facebook/react) for building the UI 8 | * [react-router - 3.x](https://github.com/rackt/react-router) for UI routing 9 | * [webpack - 1.x](https://github.com/webpack/webpack) as the client side module loader 10 | * [babel - 6.x](https://github.com/babel/babel) for compiling the ES6/JSX code 11 | 12 | ## tl;dr - to run the example 13 | ```shell 14 | # cd `into_this_dir` 15 | $ npm install 16 | $ npm start # or `npm run dev` 17 | $ open http://localhost:3000 18 | ``` 19 | 20 | ## step by step walkthrough to build the app 21 | 22 | ### step 1 23 | ```shell 24 | # let us start by installing the dependencies for our app 25 | # create a npm manifest 26 | # (fill out the needed information like name, author, etc..) 27 | $ npm init 28 | 29 | # install express, react, react-router (optional) & react-engine 30 | $ npm install express@4 react-engine@4 react@16 react-dom@16 react-router@3 --save 31 | 32 | # install the rest of the dependencies 33 | $ npm install babel-register@6 babel-preset-react@6 webpack@1 --save 34 | 35 | # we are going to use a static json file that contains 36 | # an array of movie information as the data source for 37 | # our movie catalog app 38 | $ touch movies.json 39 | # copy the contents for this file from http://bit.ly/2BbcpIb 40 | ``` 41 | 42 | ### step 2 43 | ```shell 44 | # next, let us build the client side of our app 45 | # create a directory called public and inside that 46 | # create the client side index file 47 | $ mkdir public 48 | $ touch public/index.js 49 | 50 | # create a directory called views to hold all the view files 51 | # also create a client side routes file to hold the react-router routes 52 | $ mkdir public/views 53 | $ touch public/routes.jsx 54 | 55 | # setup the client side react-engine inside public/index.js 56 | # instructions: https://github.com/paypal/react-engine#usage-on-client-side-mounting 57 | # public/index.js code for our app can be found here - http://bit.ly/2DLbIIp 58 | ``` 59 | 60 | ### step 3 61 | ```js 62 | // since we are building a movie catalog app, let us plan to have two UI pages. 63 | // 1. list page - to list the catalog of movies 64 | // 2. detail page - to show the detailed description of a movie 65 | 66 | // lets start building the react-router route file (public/routes.jsx) 67 | // keeping in mind the above requirements 68 | const Layout = require('./views/layout.jsx'); 69 | const ListPage = require('./views/list.jsx'); 70 | const DetailPage = require('./views/detail.jsx'); 71 | 72 | const routes = module.exports = ( 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | ); 81 | ``` 82 | 83 | ### step 4 84 | ```shell 85 | # next, lets build the actual UI inside the pages 86 | # create the below three files inside the public/views directory 87 | $ touch public/views/layout.jsx 88 | $ touch public/views/list.jsx 89 | $ touch public/views/detail.jsx 90 | ``` 91 | 92 | ```js 93 | // public/views/layout.jsx file contains the main parts of the app 94 | // such as html, body and script tags. 95 | module.exports = (props) => { 96 | return ( 97 | 98 | 99 | 100 | React Engine Example App 101 | 102 | 103 | 104 |
105 | {/* Router now automatically populates props.children of your components based on the active route. https://github.com/rackt/react-router/blob/latest/CHANGES.md#routehandler */} 106 | {props.children} 107 |
108 | 109 | 110 | 111 | ); 112 | }; 113 | 114 | // public/views/list.jsx file contains the catalog view elements of our app. 115 | // we iterate through the array of movies and display them on this page. 116 | module.exports = (props) => { 117 | return ( 118 |
119 |

Movies

120 |
Click on a movie to see the details
121 |
    122 | {props.movies.map((movie) => { 123 | return ( 124 |
  • 125 | 126 | {movie.title} 127 | 128 |
  • 129 | ); 130 | })} 131 |
132 |
133 | ); 134 | }; 135 | 136 | // public/views/detail.jsx file contains the markup to 137 | // display the detail information of a movie 138 | module.exports = (props) => { 139 | const movieId = props.params.id; 140 | const movie = props.movies.find((_movie) => _movie.id === movieId); 141 | 142 | return ( 143 |
144 |

{movie.title}

145 | {movie.title} 146 | more info 147 |
148 | ); 149 | }; 150 | ``` 151 | 152 | ### step 5 153 | ```shell 154 | # next, lets add the server side file 155 | $ touch index.js 156 | ``` 157 | 158 | ```js 159 | // start by configuring Babel at the top 160 | // this takes care of parsing JSX files and also ES6 code 161 | require('babel-register')({ 162 | presets: ['react'] 163 | }); 164 | 165 | // next, lets create the express app 166 | const express = require('express'); 167 | const renderer = require('react-engine'); 168 | const app = express(); 169 | 170 | // then create the view engine for our express app 171 | const reactRoutesFilePath = path.join(__dirname + '/public/routes.jsx'); 172 | const engine = renderer.server.create({ 173 | routes: require(reactRoutesFilePath), 174 | routesFilePath: reactRoutesFilePath 175 | }); 176 | 177 | // then configure our express app with the view engine that we created 178 | // set the engine 179 | app.engine('.jsx', engine); 180 | // set the view directory 181 | app.set('views', path.join(__dirname, '/public/views')); 182 | // set jsx as the view engine 183 | app.set('view engine', 'jsx'); 184 | // finally, set the custom view 185 | app.set('view', renderer.expressView); 186 | 187 | // next, lets configure the routes for the express app 188 | // expose public folder as static assets (JS/CSS) 189 | app.use(express.static(path.join(__dirname, '/public'))); 190 | // add the our app routes 191 | // we open a free pass to all GET requests to our app and use react-engine to render them 192 | app.get('*', (req, res) => { 193 | res.render(req.url, { 194 | movies: require('./movies.json') 195 | }); 196 | }); 197 | 198 | // add the error handler middleware 199 | app.use((err, req, res, next) => { 200 | console.error(err); 201 | 202 | // http://expressjs.com/en/guide/error-handling.html 203 | if (res.headersSent) { 204 | return next(err); 205 | } 206 | 207 | if (err._type && err._type === ReactEngine.reactRouterServerErrors.MATCH_REDIRECT) { 208 | return res.redirect(302, err.redirectLocation); 209 | } 210 | else if (err._type && err._type === ReactEngine.reactRouterServerErrors.MATCH_NOT_FOUND) { 211 | return res.status(404).send('Route Not Found!'); 212 | } 213 | else { 214 | // for ReactEngine.reactRouterServerErrors.MATCH_INTERNAL_ERROR or 215 | // any other error we just send the error message back 216 | return res.status(500).send(err.message); 217 | } 218 | }); 219 | 220 | // the last step in the server side is to configure the express app to listen on port 3000 221 | app.listen(3000, () => { 222 | console.log('Example app listening at http://localhost:%s', PORT); 223 | }); 224 | 225 | // the consolidated full code for this file can be 226 | // found here: http://bit.ly/1MdzR5c 227 | ``` 228 | 229 | ### step 6 230 | ```shell 231 | # finally, lets configure webpack, our client side module loader 232 | # we need two webpack loaders for our app 233 | # 1. babel-loader for webpack to load jsx and es6 code 234 | # 2. json-loader for webpack to load json files 235 | $ npm install babel-loader@6 json-loader@0.5 --save 236 | 237 | # next, add a webpack configuration file 238 | $ touch webpack.config.js 239 | 240 | # configure webpack to build a bundle.js file using public/index.js as the main file 241 | # module.exports = { 242 | # 243 | # entry: __dirname + '/public/index.js', 244 | # 245 | # output: { 246 | # path: __dirname + '/public', 247 | # filename: 'bundle.js' 248 | # }, 249 | # 250 | # module: { 251 | # loaders: [ 252 | # { 253 | # test: /\.jsx?$/, 254 | # exclude: /node_modules/, 255 | # loader: 'babel?presets[]=react' 256 | # }, 257 | # { 258 | # test: /\.json$/, 259 | # loader: 'json-loader' 260 | # } 261 | # ] 262 | # }, 263 | # 264 | # resolve: { 265 | # extensions: ['', '.js', '.jsx', '.json'] 266 | # } 267 | # }; 268 | ``` 269 | 270 | ### step 7 271 | ```shell 272 | # modify the public/views/layout.jsx file to add the bundle.js into it 273 | # 274 | 275 | # also lets add a start script to our package.json to build our client code using webpack and then start the app 276 | # "scripts": { 277 | # "start": "webpack && node index.js" 278 | # } 279 | 280 | # now that we are done with the app, 281 | # lets start the app and launch http://localhost:3000 in a browser 282 | $ npm start 283 | ``` 284 | 285 | ### misc 286 | ```shell 287 | # to beautify our movie catalog app we are going to add some css 288 | # modify the public/views/layout.jsx file to add the styles.css into it 289 | # 290 | $ touch public/styles.css 291 | # copy the contents for this file from http://bit.ly/2mQoU7n 292 | ``` 293 | -------------------------------------------------------------------------------- /examples/movie catalog/index.js: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | 'use strict'; 17 | 18 | require('babel-register')({ 19 | presets: ['es2015', 'react'] 20 | }); 21 | 22 | require('./server'); 23 | -------------------------------------------------------------------------------- /examples/movie catalog/movies.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "1", 4 | "title": "Ocean's 11", 5 | "url": "https://en.wikipedia.org/wiki/Ocean%27s_11", 6 | "image": "https://upload.wikimedia.org/wikipedia/en/6/68/Ocean%27s_Eleven_2001_Poster.jpg" 7 | }, 8 | { 9 | "id": "2", 10 | "title": "The Bourne Supremacy", 11 | "url": "https://en.wikipedia.org/wiki/The_Bourne_Supremacy_(film)", 12 | "image": "https://upload.wikimedia.org/wikipedia/en/3/30/Bourne_supremacy_ver2.jpg" 13 | }, 14 | { 15 | "id": "3", 16 | "title": "500 Days of Summer", 17 | "url": "https://en.wikipedia.org/wiki/500_Days_of_Summer", 18 | "image": "https://upload.wikimedia.org/wikipedia/en/d/d1/Five_hundred_days_of_summer.jpg" 19 | }, 20 | { 21 | "id": "4", 22 | "title": "The Terminal", 23 | "url": "https://en.wikipedia.org/wiki/The_Terminal", 24 | "image": "https://upload.wikimedia.org/wikipedia/en/8/86/Movie_poster_the_terminal.jpg" 25 | }, 26 | { 27 | "id": "5", 28 | "title": "Mission: Impossible – Rogue Nation", 29 | "url": "https://en.wikipedia.org/wiki/Mission:_Impossible_%E2%80%93_Rogue_Nation", 30 | "image": "https://upload.wikimedia.org/wikipedia/en/f/fb/Mission_Impossible_Rogue_Nation_poster.jpg" 31 | }, 32 | { 33 | "id": "6", 34 | "title": "Up", 35 | "url": "https://en.wikipedia.org/wiki/Up_(2009_film)", 36 | "image": "https://upload.wikimedia.org/wikipedia/en/0/05/Up_%282009_film%29.jpg" 37 | }, 38 | { 39 | "id": "7", 40 | "title": "Elf", 41 | "url": "https://en.wikipedia.org/wiki/Elf_(film)", 42 | "image": "https://upload.wikimedia.org/wikipedia/en/d/df/Elf_movie.jpg" 43 | }, 44 | { 45 | "id": "8", 46 | "title": "A Christmas Carol", 47 | "url": "https://en.wikipedia.org/wiki/A_Christmas_Carol_(1938_film)", 48 | "image": "https://upload.wikimedia.org/wikipedia/commons/f/ff/CCPoster_art-1938.jpg" 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /examples/movie catalog/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-engine-example-app", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "webpack && node index.js", 7 | "dev": "webpack -w & nodemon index.js" 8 | }, 9 | "author": "Sam Selvanathan (sjasel@gmail.com)", 10 | "dependencies": { 11 | "babel-core": "^6.3.17", 12 | "babel-loader": "^6.2.0", 13 | "babel-preset-es2015": "^6.3.13", 14 | "babel-preset-react": "^6.3.13", 15 | "babel-register": "^6.3.13", 16 | "express": "^4.13.3", 17 | "json-loader": "^0.5.4", 18 | "react": "^16.2.0", 19 | "react-dom": "^16.2.0", 20 | "react-engine": "^4.5.0", 21 | "react-router": "^3.2.0", 22 | "serve-favicon": "^2.3.0", 23 | "webpack": "^1.12.9" 24 | }, 25 | "devDependencies": { 26 | "nodemon": "^1.9.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/movie catalog/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paypal/react-engine/96e371cb5e2484dbe637492f7aa218127fd0ca06/examples/movie catalog/public/favicon.ico -------------------------------------------------------------------------------- /examples/movie catalog/public/index.js: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | 'use strict'; 17 | 18 | // import the react-router routes 19 | const Routes = require('./routes.jsx'); 20 | 21 | // import the react-engine's client side booter 22 | const ReactEngineClient = require('react-engine/lib/client'); 23 | 24 | // boot options 25 | const options = { 26 | routes: Routes, 27 | 28 | // supply a function that can be called 29 | // to resolve the file that was rendered. 30 | viewResolver: (viewName) => require('./views/' + viewName), 31 | }; 32 | 33 | document.addEventListener('DOMContentLoaded', () => { 34 | // boot the app when the DOM is ready 35 | ReactEngineClient.boot(options); 36 | }); 37 | -------------------------------------------------------------------------------- /examples/movie catalog/public/routes.jsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | import React from 'react'; 17 | import { Router, Route, IndexRoute, Redirect, browserHistory } from 'react-router'; 18 | 19 | import Layout from './views/layout.jsx'; 20 | import ListPage from './views/list.jsx'; 21 | import DetailPage from './views/detail.jsx'; 22 | import Error404 from './views/404.jsx'; 23 | 24 | module.exports = ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | -------------------------------------------------------------------------------- /examples/movie catalog/public/styles.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | background: #8E0E00; /* fallback for old browsers */ 7 | background: -webkit-linear-gradient(to left, #8E0E00 , #1F1C18); /* Chrome 10-25, Safari 5.1-6 */ 8 | background: linear-gradient(to left, #8E0E00 , #1F1C18); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ 9 | color: #fff; 10 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 11 | -webkit-font-smoothing: antialiased; 12 | -webkit-tap-highlight-color: transparent; 13 | font-size: 87.5%; 14 | line-height: 1.5em; 15 | } 16 | 17 | h1 { 18 | text-shadow: 0 0 100px rgba(0,0,0,.5); 19 | letter-spacing: 0.02em; 20 | } 21 | 22 | ul { 23 | list-style-type: none; 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | img { 29 | border-radius: 2px; 30 | position: relative; 31 | box-shadow: 6px 3px 24px -4px rgba(0,0,0,0.75); 32 | } 33 | 34 | body > div { 35 | margin: 10px auto; 36 | width: 800px; 37 | } 38 | 39 | h6 { 40 | text-decoration: underline; 41 | } 42 | 43 | li { 44 | float: left; 45 | margin: 0 10px 10px 0; 46 | } 47 | 48 | #list img { 49 | width: 140px; 50 | height: 201px; 51 | } 52 | 53 | #detail { 54 | width: 400px; 55 | margin: 60px auto; 56 | } 57 | 58 | #detail img { 59 | width: 260px; 60 | height: 382px; 61 | } 62 | 63 | #detail a { 64 | display: block; 65 | text-align: center; 66 | font-size: 140%; 67 | color: #fff; 68 | } 69 | -------------------------------------------------------------------------------- /examples/movie catalog/public/views/404.jsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | 'use strict'; 17 | 18 | const React = require('react'); 19 | 20 | module.exports = (props) => { 21 | return ( 22 |

URL: {props.location.pathname} - Not Found(404)

23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /examples/movie catalog/public/views/500.jsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | 'use strict'; 17 | 18 | const React = require('react'); 19 | 20 | module.exports = (props) => { 21 | return ( 22 |
23 |

Internal Service Error (500)

24 |

Error message: {props.err.message}

25 | {props.err.stack} 26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /examples/movie catalog/public/views/detail.jsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | 'use strict'; 17 | 18 | const React = require('react'); 19 | 20 | module.exports = (props) => { 21 | const movieId = props.params.id; 22 | const movie = props.movies.find((_movie) => _movie.id === movieId); 23 | 24 | return ( 25 |
26 |

{movie.title}

27 | {movie.title} 28 | more info 29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /examples/movie catalog/public/views/layout.jsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | 'use strict'; 17 | 18 | const React = require('react'); 19 | 20 | module.exports = (props) => { 21 | return ( 22 | 23 | 24 | 25 | React Engine Example App 26 | 27 | 28 | 29 |
30 | {/* Router now automatically populates props.children of your components based on the active route. https://github.com/rackt/react-router/blob/latest/CHANGES.md#routehandler */} 31 | {props.children} 32 |
33 | 34 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /examples/movie catalog/public/views/list.jsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | 'use strict'; 17 | 18 | const React = require('react'); 19 | const Router = require('react-router'); 20 | 21 | module.exports = (props) => { 22 | return ( 23 |
24 |

Movies

25 |
Click on a movie to see the details
26 |
    27 | {props.movies.map((movie) => { 28 | return ( 29 |
  • 30 | 31 | {movie.title} 32 | 33 |
  • 34 | ); 35 | })} 36 | 37 |
38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /examples/movie catalog/server.js: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | 'use strict'; 17 | 18 | const PORT = 3000; 19 | 20 | import { join } from 'path'; 21 | import express from 'express'; 22 | import favicon from 'serve-favicon'; 23 | import ReactEngine from 'react-engine'; 24 | import movies from './movies.json'; 25 | import routes from './public/routes.jsx'; 26 | 27 | let app = express(); 28 | 29 | // create the view engine with `react-engine` 30 | let engine = ReactEngine.server.create({ 31 | routes, 32 | routesFilePath: join(__dirname, '/public/routes.jsx'), 33 | performanceCollector: (stats) => { 34 | console.log(stats); 35 | } 36 | }); 37 | 38 | // set the engine 39 | app.engine('.jsx', engine); 40 | 41 | // set the view directory 42 | app.set('views', join(__dirname, '/public/views')); 43 | 44 | // set jsx as the view engine 45 | app.set('view engine', 'jsx'); 46 | 47 | // finally, set the custom view 48 | app.set('view', ReactEngine.expressView); 49 | 50 | // expose public folder as static assets 51 | app.use(express.static(join(__dirname, '/public'))); 52 | 53 | app.use(favicon(join(__dirname, '/public/favicon.ico'))); 54 | 55 | // add our app routes 56 | app.get('*', (req, res) => { 57 | res.render(req.url, { movies }); 58 | }); 59 | 60 | app.use((err, req, res, next) => { 61 | console.error(err); 62 | 63 | // http://expressjs.com/en/guide/error-handling.html 64 | if (res.headersSent) { 65 | return next(err); 66 | } 67 | 68 | if (err._type && err._type === ReactEngine.reactRouterServerErrors.MATCH_REDIRECT) { 69 | return res.redirect(302, err.redirectLocation); 70 | } 71 | else if (err._type && err._type === ReactEngine.reactRouterServerErrors.MATCH_NOT_FOUND) { 72 | return res.status(404).render(req.url); 73 | } 74 | else { 75 | // for ReactEngine.reactRouterServerErrors.MATCH_INTERNAL_ERROR or 76 | // any other error we just send the error message back 77 | return res.status(500).render('500.jsx', { 78 | err: { 79 | message: err.message, 80 | stack: err.stack 81 | } 82 | }); 83 | } 84 | }); 85 | 86 | app.listen(PORT, () => { 87 | console.log('Example app listening at http://localhost:%s', PORT); 88 | }); 89 | -------------------------------------------------------------------------------- /examples/movie catalog/webpack.config.js: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | 'use strict'; 17 | 18 | module.exports = { 19 | entry: __dirname + '/public/index.js', 20 | output: { 21 | path: __dirname + '/public', 22 | filename: 'bundle.js' 23 | }, 24 | module: { 25 | loaders: [ 26 | { 27 | test: /\.jsx?$/, 28 | exclude: /node_modules/, 29 | loader: 'babel-loader', 30 | query: { 31 | presets: ['react', 'es2015'] 32 | } 33 | }, 34 | { 35 | test: /\.json$/, 36 | loader: 'json-loader' 37 | } 38 | ] 39 | }, 40 | resolve: { 41 | extensions: ['', '.js', '.jsx', '.json'] 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | 'use strict'; 17 | 18 | exports.server = require('./lib/server'); 19 | exports.client = require('./lib/client'); 20 | exports.expressView = require('./lib/expressView'); 21 | exports.reactRouterServerErrors = require('./lib/reactRouterServerErrors'); 22 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | 'use strict'; 17 | 18 | var Config = require('./config.json'); 19 | var ReactDOM = require('react-dom'); 20 | var assign = require('lodash/assign'); 21 | var isFunction = require('lodash/isFunction'); 22 | 23 | // declaring like this helps in unit test 24 | // dependency injection using `rewire` module 25 | var _window; 26 | var _document; 27 | if (typeof window !== 'undefined' && typeof document !== 'undefined') { 28 | _window = window; 29 | _document = document; 30 | } 31 | 32 | // returns the data/state that was 33 | // injected by server during rendering 34 | var data = exports.data = function data() { 35 | // this file needs to be a external js file 36 | var element = document.getElementById(Config.client.markupId); 37 | // grab the contents from the script element 38 | var jsonString = element.textContent || element.innerText; 39 | // parse the text contents to JSON 40 | return JSON.parse(jsonString); 41 | }; 42 | 43 | // the client side boot function 44 | exports.boot = function boot(options, callback) { 45 | 46 | var React = require('react'); 47 | var Router; 48 | var RouterComponent; 49 | var match; 50 | var browserHistory; 51 | 52 | try { 53 | Router = require('react-router'); 54 | RouterComponent = Router.Router; 55 | match = Router.match; 56 | 57 | // compatibility for both `react-router` v2 and v1 58 | browserHistory = Router.browserHistory || require('history').createHistory(); 59 | } catch (err) { 60 | if (!Router && options.routes) { 61 | throw new Error('asking to use react router for rendering, but no routes are provided'); 62 | } 63 | } 64 | 65 | var router; 66 | var history; 67 | var location; 68 | var viewResolver = options.viewResolver; 69 | 70 | // pick up the state that was injected by server during rendering 71 | var props = data(); 72 | var useRouter = (props.__meta.view === null); 73 | var mountNode = options.mountNode || _document; 74 | 75 | // wrap component with react-redux Proivder if redux is required 76 | var wrap = function(component) { 77 | if (options.reduxStoreInitiator && isFunction(options.reduxStoreInitiator)) { 78 | var initStore = options.reduxStoreInitiator; 79 | if (initStore.default) { 80 | initStore = initStore.default; 81 | } 82 | var store = initStore(props); 83 | var Provider = require('react-redux').Provider; 84 | return React.createElement(Provider, { store: store }, component); 85 | } else { 86 | return component; 87 | } 88 | }; 89 | 90 | var renderMethod = ReactDOM.hydrate || ReactDOM.render; 91 | 92 | if (useRouter) { 93 | 94 | history = options.history || browserHistory; 95 | location = _window.location.pathname + 96 | _window.location.search + _window.location.hash; 97 | 98 | if (options.routes.default) { 99 | options.routes = options.routes.default; 100 | } 101 | 102 | // Wrap the 'render' function within a call to 'match'. This is a workaround to support 103 | // users using code splitting functionality 104 | match({ routes: options.routes, location: location }, function() { 105 | 106 | // for any component created by react-router, merge model data with the routerProps 107 | // NOTE: This may be imposing too large of an opinion? 108 | var routerComponent = React.createElement(RouterComponent, { 109 | createElement: function(Component, routerProps) { 110 | return React.createElement(Component, assign({}, props, routerProps)); 111 | }, 112 | 113 | routes: options.routes, 114 | history: history 115 | }); 116 | 117 | // wrap routerComponent with redux provider 118 | renderMethod(wrap(routerComponent), mountNode); 119 | }); 120 | 121 | } else { 122 | // get the file from viewResolver supplying it with a view name 123 | var view = viewResolver(props.__meta.view); 124 | 125 | // create a react view factory 126 | var viewFactory = React.createFactory(view); 127 | 128 | // render the factory on the client 129 | // doing this, sets up the event 130 | // listeners and stuff aka mounting views. 131 | renderMethod(wrap(viewFactory(props)), mountNode); 132 | } 133 | 134 | // call the callback with the data that was used for rendering 135 | return callback && callback(props, history); 136 | }; 137 | -------------------------------------------------------------------------------- /lib/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "docType": "", 3 | "defaultKeysToFilter": ["settings", "enrouten", "_locals"], 4 | "client": { 5 | "markupId": "react-engine-props" 6 | }, 7 | "staticMarkup": false, 8 | "scriptType": "application/json" 9 | } 10 | -------------------------------------------------------------------------------- /lib/expressView.js: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | 'use strict'; 17 | 18 | var util = require('./util'); 19 | var format = require('util').format; 20 | var inherit = require('util').inherits; 21 | var debug = require('debug')(require('../package').name); 22 | var url = require('url'); 23 | 24 | var View = util.safeRequire('express/lib/view'); 25 | 26 | function ReactEngineView(name, options) { 27 | debug(format('ReactEngineView :constructor: name: %s and options: %j', 28 | name, options)); 29 | 30 | // when the view name starts with `/` we assume 31 | // that we need to use react router to render. 32 | this.useRouter = (name[0] === '/'); 33 | 34 | if (this.useRouter) { 35 | name = url.parse(name).path; 36 | } 37 | 38 | View.call(this, name, options); 39 | } 40 | 41 | // inherit form express view 42 | inherit(ReactEngineView, View); 43 | 44 | ReactEngineView.prototype.lookup = function lookup(name) { 45 | debug(format('ReactEngineView :lookup: name: %s', name)); 46 | if (this.useRouter) { 47 | return name; 48 | } else { 49 | return View.prototype.lookup.call(this, name); 50 | } 51 | }; 52 | 53 | ReactEngineView.prototype.render = function render(options, fn) { 54 | debug(format('ReactEngineView :render:')); 55 | if (this.useRouter) { 56 | this.engine(this.name, options, fn); 57 | } else { 58 | return View.prototype.render.call(this, options, fn); 59 | } 60 | }; 61 | 62 | module.exports = ReactEngineView; 63 | -------------------------------------------------------------------------------- /lib/performance.js: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | 'use strict'; 17 | 18 | module.exports = function Performance(name) { 19 | 20 | var startTime = Date.now(); 21 | var time = process.hrtime(); 22 | 23 | return function end() { 24 | 25 | var diff = process.hrtime(time); 26 | 27 | // duration in milliseconds 28 | var duration = diff[0] + (diff[1] / 1000000); 29 | 30 | return { 31 | name: name, 32 | startTime: startTime, 33 | endTime: Date.now(), 34 | duration: duration 35 | }; 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /lib/reactRouterServerErrors.js: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | 'use strict'; 17 | 18 | var errorTypes = ['MATCH_REDIRECT', 19 | 'MATCH_NOT_FOUND', 20 | 'MATCH_INTERNAL_ERROR']; 21 | 22 | var properties = {}; 23 | 24 | errorTypes.map(function(errorType) { 25 | properties[errorType] = { 26 | configurable: false, 27 | writable: false, 28 | enumerable: true, 29 | value: errorType 30 | }; 31 | }); 32 | 33 | /* 34 | export the reactRouterServerErrors object 35 | { 36 | MATCH_REDIRECT: 'MATCH_REDIRECT', 37 | MATCH_NOT_FOUND: 'MATCH_NOT_FOUND', 38 | MATCH_INTERNAL_ERROR: 'MATCH_INTERNAL_ERROR' 39 | } 40 | */ 41 | module.exports = Object.create(null, properties); 42 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | 'use strict'; 17 | 18 | var isString = require('lodash/isString'); 19 | var assign = require('lodash/assign'); 20 | var unset = require('lodash/unset'); 21 | var path = require('path'); 22 | var util = require('./util'); 23 | var assert = require('assert'); 24 | var Config = require('./config.json'); 25 | var jsesc = require('jsesc'); 26 | var ReactDOMServer = require('react-dom/server'); 27 | var debug = require('debug')(require('../package').name); 28 | var ReactRouterServerErrors = require('./reactRouterServerErrors'); 29 | 30 | var format = require('util').format; 31 | var Performance = require('./performance'); 32 | 33 | // safely require the peer-dependencies 34 | var React = util.safeRequire('react'); 35 | 36 | function generateReactRouterServerError(type, existingErrorObj, additionalProperties) { 37 | var err = existingErrorObj || new Error('react router match fn error'); 38 | err._type = type; 39 | if (additionalProperties) { 40 | assign(err, additionalProperties); 41 | } 42 | 43 | return err; 44 | } 45 | 46 | exports.create = function create(createOptions) { 47 | createOptions = createOptions || {}; 48 | 49 | // safely require the peer-dependencies 50 | var React = util.safeRequire('react'); 51 | var Router; 52 | var match; 53 | var RouterContext; 54 | 55 | try { 56 | Router = require('react-router'); 57 | match = Router.match; 58 | 59 | // compatibility for both `react-router` v2 and v1 60 | RouterContext = Router.RouterContext || Router.RoutingContext; 61 | } catch (err) { 62 | if (!Router && createOptions.routes) { 63 | throw err; 64 | } 65 | } 66 | 67 | createOptions.scriptType = isString(createOptions.scriptType) ? createOptions.scriptType : Config.scriptType; 68 | createOptions.docType = isString(createOptions.docType) ? createOptions.docType : Config.docType; 69 | createOptions.renderOptionsKeysToFilter = createOptions.renderOptionsKeysToFilter || []; 70 | createOptions.staticMarkup = createOptions.staticMarkup !== undefined ? createOptions.staticMarkup : Config.staticMarkup; 71 | 72 | assert(Array.isArray(createOptions.renderOptionsKeysToFilter), 73 | '`renderOptionsKeysToFilter` - should be an array'); 74 | 75 | createOptions.renderOptionsKeysToFilter = 76 | createOptions.renderOptionsKeysToFilter.concat(Config.defaultKeysToFilter); 77 | 78 | if (createOptions.performanceCollector) { 79 | assert.equal(typeof createOptions.performanceCollector, 80 | 'function', 81 | '`performanceCollector` - should be a function'); 82 | } 83 | 84 | // the render implementation 85 | return function render(thing, options, callback) { 86 | 87 | var perfInstance; 88 | 89 | if (createOptions.performanceCollector) { 90 | perfInstance = Performance(thing); 91 | } 92 | 93 | function done(err, html) { 94 | if (!options.settings['view cache']) { 95 | // remove all the files under the express's view folder from require cache. 96 | // Helps in making changes to react views without restarting the server. 97 | util.clearRequireCache(createOptions.routesFilePath); 98 | util.clearRequireCacheInDir(options.settings.views, options.settings['view engine']); 99 | } 100 | 101 | if (createOptions.performanceCollector) { 102 | createOptions.performanceCollector(perfInstance()); 103 | } 104 | 105 | callback(err, html); 106 | } 107 | 108 | function renderAndDecorate(component, data, html) { 109 | if (createOptions.staticMarkup) { 110 | // render the component to static markup 111 | html += ReactDOMServer.renderToStaticMarkup(component); 112 | } else { 113 | // render the redux wrapped component 114 | if (createOptions.reduxStoreInitiator) { 115 | // add redux provider 116 | var Provider = require('react-redux').Provider; 117 | var initStore; 118 | try { 119 | initStore = require(createOptions.reduxStoreInitiator); 120 | if (initStore.default) { 121 | initStore = initStore.default; 122 | } 123 | var store = initStore(data); 124 | var wrappedComponent = React.createElement(Provider, { store: store }, component); 125 | // render the component 126 | html += ReactDOMServer.renderToString(wrappedComponent); 127 | } catch (err) { 128 | return done(err); 129 | } 130 | } else { 131 | // render the component 132 | html += ReactDOMServer.renderToString(component); 133 | } 134 | 135 | // the `script` tag that gets injected into the server rendered pages. 136 | // https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet#RULE_.233_-_JavaScript_Escape_Before_Inserting_Untrusted_Data_into_JavaScript_Data_Values 137 | var openScriptTag = `'; 147 | 148 | 149 | if (createOptions.docType === '') { 150 | // if the `docType` is empty, the user did not want to add a docType to the rendered component, 151 | // which means they might not be rendering a full page with `html` and `body` tags 152 | // so attach the script tag to just the end of the generated html string 153 | html += script; 154 | } 155 | else { 156 | var htmlTag = createOptions.scriptLocation === 'head' ? '' : ''; 157 | html = html.replace(htmlTag, script + htmlTag); 158 | } 159 | } 160 | 161 | return html; 162 | } 163 | 164 | if (createOptions.routes && createOptions.routesFilePath) { 165 | // if `routesFilePath` property is provided, then in 166 | // cases where 'view cache' is false, the routes are reloaded for every render. 167 | createOptions.routes = require(createOptions.routesFilePath); 168 | if (createOptions.routes.default) { 169 | createOptions.routes = createOptions.routes.default; 170 | } 171 | } 172 | 173 | // initialize the markup string 174 | var html = createOptions.docType; 175 | 176 | // create the data object that will be fed into the React render method. 177 | // Data is a mash of the express' `render options` and `res.locals` 178 | // and meta info about `react-engine` 179 | var data = assign({ 180 | __meta: { 181 | // get just the relative path for view file name 182 | view: null, 183 | markupId: Config.client.markupId 184 | } 185 | }, options); 186 | if (this.useRouter && !createOptions.routes) { 187 | return done(new Error('asking to use react router for rendering, but no routes are provided')); 188 | } 189 | 190 | // since `unset` mutates the obj, lets clone a copy 191 | // Also, we are using JSON.parse(JSON.stringify(data)) to clone the object super fast. 192 | // a valid assumption in using this method of cloning at this point: we have only variables 193 | // and not any functions in data object - so need for lodash cloneDeep 194 | try { 195 | data = JSON.parse(JSON.stringify(data)); 196 | createOptions.renderOptionsKeysToFilter.forEach(function(key) { 197 | unset(data, key); 198 | }); 199 | } catch (parseErr) { 200 | return done(parseErr); 201 | } 202 | 203 | try { 204 | if (this.useRouter) { 205 | return match({ routes:createOptions.routes, location:thing}, function reactRouterMatchHandler(error, redirectLocation, renderProps) { 206 | if (error) { 207 | debug('server.js match 500 %s', error.message); 208 | var err = generateReactRouterServerError(ReactRouterServerErrors.MATCH_INTERNAL_ERROR, error); 209 | return done(err); 210 | } else if (redirectLocation) { 211 | debug('server.js match 302 %s', redirectLocation.pathname + redirectLocation.search); 212 | var err = generateReactRouterServerError(ReactRouterServerErrors.MATCH_REDIRECT, null, { 213 | redirectLocation: redirectLocation.pathname + redirectLocation.search 214 | }); 215 | return done(err); 216 | } else if (renderProps) { 217 | renderProps.createElement = function(Component, routerProps) { 218 | // Other than fusing the data object with the routerProps, there is no way 219 | // to pass data into the routing context of react-router during a server render. 220 | // since we are going to use `assign` to fuse the routerProps and the actual 221 | // data object, we need to make sure that there are no properties between the two object 222 | // with the same name at the root level. (Having two properties with the same name breaks assign.) 223 | // Info on why we need to fuse the two objects? 224 | // -------------------------------------------- 225 | // * https://github.com/ngduc/react-setup/issues/10 226 | // * https://github.com/reactjs/react-router/issues/1969 227 | // * http://stackoverflow.com/questions/36137901/react-route-and-server-side-rendering-how-to-render-components-with-data 228 | if (options.settings.env !== 'production') { 229 | var intersection = Object.keys(routerProps).filter(function(elem) { 230 | return Object.keys(data).indexOf(elem) !== -1; 231 | }); 232 | if (intersection.length) { 233 | var errMsg = 'Your data object cannot have property(ies) named: "' + 234 | intersection + 235 | '"\n Blacklisted property names that cannot be used: "' + 236 | Object.keys(routerProps) + 237 | '"\n' 238 | throw new Error(errMsg); 239 | } 240 | } 241 | 242 | // define a createElement strategy for react-router that transfers data props to all route "components" 243 | // for any component created by react-router, fuse data object with the routerProps 244 | // NOTE: This may be imposing too large of an opinion? 245 | return React.createElement(Component, assign({}, data, routerProps)); 246 | }; 247 | 248 | return done(null, renderAndDecorate(React.createElement(RouterContext, renderProps), data, html)); 249 | } else { 250 | debug('server.js match 404'); 251 | var err = generateReactRouterServerError(ReactRouterServerErrors.MATCH_NOT_FOUND); 252 | return done(err); 253 | } 254 | }); 255 | } 256 | else { 257 | // path utility to make path string compatible in different OS 258 | // ------------------------------------------------------------ 259 | // use `path.normalize()` to normalzie absolute view file path and absolute base directory path 260 | // to prevent path strings like `/folder1/folder2/../../folder3/exampleFile` 261 | // then, derive relative view file path 262 | // and replace backslash with slash to be compatible on Windows 263 | data.__meta.view = path.normalize(thing) 264 | .replace(path.normalize(options.settings.views), '').substring(1) 265 | .replace('\\', '/'); 266 | 267 | var view = require(thing); 268 | 269 | // Check for an ES6 `default` property on the module export 270 | // ------------------------------------------------------------ 271 | // TypeScript and Babel users that leverage ES6 module depend on this 272 | // e.g. `export default function MyView() {};` 273 | if (view.default) { 274 | view = view.default; 275 | } 276 | 277 | // create the Component using react's createFactory 278 | var component = React.createFactory(view); 279 | return done(null, renderAndDecorate(component(data), data, html)); 280 | } 281 | } 282 | catch (err) { 283 | 284 | // on error, pass to the next 285 | // middleware in the chain! 286 | return done(err); 287 | } 288 | }; 289 | }; 290 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | 'use strict'; 17 | 18 | var _require; 19 | var glob = require('glob'); 20 | var path = require('path'); 21 | var format = require('util').format; 22 | var debug = require('debug')(require('../package.json').name); 23 | 24 | function clearRequireCache(file) { 25 | delete require.cache[file]; 26 | } 27 | 28 | // clears require cache of files that have 29 | // extension `extension` from a given directory `dir`. 30 | function clearRequireCacheInDir(dir, extension) { 31 | 32 | var options = { 33 | cwd: dir 34 | }; 35 | 36 | // find all files with the `extension` in the express view directory 37 | // and clean them out of require's cache. 38 | var files = glob.sync('**/*.' + extension, options); 39 | 40 | files.map(function(file) { 41 | clearRequireCache(dir + path.sep + (file.split(/\\|\//g).join(path.sep))); 42 | }); 43 | } 44 | 45 | // workaround when `npm link`'ed for development 46 | // Force Node to load modules from linking parent. 47 | // https://github.com/npm/npm/issues/5875 48 | // plus React doesn't LIKE (at all) when 49 | // multiple copies of React are used around 50 | // https://github.com/facebook/react/issues/1939 51 | function safeRequire(name) { 52 | 53 | var module; 54 | 55 | try { 56 | module = require(name); 57 | } 58 | catch (err) { 59 | // lazy load the module 60 | if (!_require) { 61 | _require = require('parent-require'); 62 | } 63 | 64 | debug(format('%j', err)); 65 | module = _require(name); 66 | } 67 | 68 | return module; 69 | } 70 | 71 | exports.safeRequire = safeRequire; 72 | exports.clearRequireCache = clearRequireCache; 73 | exports.clearRequireCacheInDir = clearRequireCacheInDir; 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-engine", 3 | "version": "4.5.1", 4 | "description": "a composite render engine for express apps to render both plain react views and react-router views", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npm run tape", 8 | "lint": "eslint .", 9 | "tape": "tape test/*.js | faucet" 10 | }, 11 | "engines": { 12 | "node": ">=4" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/paypal/react-engine" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/paypal/react-engine/issues" 20 | }, 21 | "publishConfig": { 22 | "registry": "https://registry.npmjs.org" 23 | }, 24 | "dependencies": { 25 | "debug": "^2.1.3", 26 | "glob": "^7.0.5", 27 | "jsesc": "^2.2.0", 28 | "lodash": "^4.13.1", 29 | "parent-require": "^1.0.0" 30 | }, 31 | "devDependencies": { 32 | "babel-core": "^6.3.26", 33 | "babel-preset-react": "^6.3.13", 34 | "babel-register": "^6.3.13", 35 | "cheerio": "^0.20.0", 36 | "eslint": "^2.13.1", 37 | "eslint-config-airbnb": "^9.0.1", 38 | "eslint-plugin-import": "^1.9.2", 39 | "eslint-plugin-jsx-a11y": "^1.5.3", 40 | "eslint-plugin-react": "^5.2.2", 41 | "express": "^4.12", 42 | "faucet": "0.0.1", 43 | "jsdom": "^9.2.1", 44 | "react": "^16.2.0", 45 | "react-dom": "^16.2.0", 46 | "react-router": "^3.2.0", 47 | "rewire": "^2.3.1", 48 | "sinon": "^1.14.1", 49 | "tape": "^4.6.0" 50 | }, 51 | "peerDependencies": { 52 | "react": "15.x.x || 16.x.x", 53 | "react-dom": "15.x.x || 16.x.x" 54 | }, 55 | "keywords": [ 56 | "react", 57 | "render", 58 | "render engine", 59 | "react-router", 60 | "view engine", 61 | "express", 62 | "jsx" 63 | ], 64 | "author": "Sam Selvanathan ", 65 | "contributors": [ 66 | "Vu Hwang ", 67 | "Robert Kuo ", 68 | "Jinto Jose ", 69 | "Jared Halpert ", 70 | "Weng Zhi Ping", 71 | "Vincent Orr ", 72 | "skarflacka", 73 | "Mark " 74 | ], 75 | "license": "Apache-2.0" 76 | } 77 | -------------------------------------------------------------------------------- /test/client.js: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | 'use strict'; 17 | 18 | var test = require('tape'); 19 | var rewire = require('rewire'); 20 | var jsdom = require('jsdom').jsdom; 21 | var DATA_MODEL = require('./server').DATA_MODEL; 22 | var DATA_MODEL_PROPS = Object.keys(DATA_MODEL); 23 | var assertions = require('./fixtures/assertions.json'); 24 | 25 | // boot options 26 | var options = { 27 | viewResolver: function(viewName) { 28 | return require('./fixtures/views/' + viewName); 29 | } 30 | }; 31 | 32 | function prepare(markup) { 33 | var client = rewire('../lib/client'); 34 | var document = jsdom(markup); 35 | var window = document.defaultView; 36 | 37 | // inject our mock window and document 38 | client.__set__('_window', window); 39 | client.__set__('_document', document); 40 | window.onerror = function(errorMsg) { 41 | throw new Error(errorMsg); 42 | }; 43 | 44 | global.document = document; 45 | global.window = window; 46 | global.navigator = { 47 | userAgent: 'tape-tests' 48 | }; 49 | 50 | return client; 51 | } 52 | 53 | function after(client) { 54 | window.onerror = null; 55 | global.document = null; 56 | global.window = null; 57 | global.navigator = null; 58 | client = null; 59 | } 60 | 61 | test('client side boot for plain react views', function(t) { 62 | var client = prepare(assertions.PROFILE_OUTPUT_WITH_REACT_ATTRS); 63 | 64 | function _boot() { 65 | 66 | client.boot(options, function(data) { 67 | // test that all properties in the DATA_MODEL exist in the received `data` 68 | // NOTE: we care only about the DATA_MODEL props and not other stuff that 69 | // might come from things like express `res.locals` 70 | // https://github.com/paypal/react-engine/blob/19cdca270c5b068f62c6436c9069e578eff7f280/lib/server.js#L65 71 | DATA_MODEL_PROPS.map(function(key) { 72 | t.notEqual(typeof data[key], 'undefined'); 73 | t.equal(data[key], DATA_MODEL[key]); 74 | }); 75 | 76 | after(client); 77 | t.end(); 78 | }); 79 | } 80 | 81 | t.doesNotThrow(_boot); 82 | }); 83 | 84 | test('client side boot throws error for invalid markup', function(t) { 85 | var client = prepare('SOME_GARBAGE_HTML'); 86 | function _boot() { 87 | client.boot(options); 88 | } 89 | 90 | t.throws(_boot); 91 | after(client); 92 | t.end(); 93 | }); 94 | -------------------------------------------------------------------------------- /test/expressView.js: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | 'use strict'; 17 | 18 | var test = require('tape'); 19 | var sinon = require('sinon'); 20 | var rewire = require('rewire'); 21 | 22 | var ExpressView = rewire('../lib/expressView'); 23 | 24 | var View = sinon.spy(require('express/lib/view')); 25 | ExpressView.__set__('View', View); 26 | 27 | // View.restore(); //todo 28 | 29 | var options = { 30 | root: __dirname, 31 | engines: { 32 | '.jsx': function() {} 33 | }, 34 | defaultEngine: 'jsx' 35 | }; 36 | 37 | test('ExpressView is a constructor function with the appropriate properties', function(t) { 38 | 39 | var expressView = new ExpressView('index', options); 40 | 41 | t.assert(View.called); 42 | t.assert(View.calledWithExactly('index', options)); 43 | t.equal(typeof ExpressView, 'function'); 44 | t.equal(typeof expressView, 'object'); 45 | t.equal(Object.keys(ExpressView.prototype).length, 2); 46 | t.equal(typeof ExpressView.prototype.lookup, 'function'); 47 | t.equal(typeof ExpressView.prototype.render, 'function'); 48 | 49 | t.end(); 50 | }); 51 | 52 | test('ExpressView `useRouter` property resolution', function(t) { 53 | 54 | var expressViewForPlainReactViewRender = new ExpressView('index', options); 55 | var expressViewForPlainReactRouterViewRender = new ExpressView('/index', options); 56 | 57 | t.equal(expressViewForPlainReactViewRender.useRouter, false); 58 | t.equal(expressViewForPlainReactRouterViewRender.useRouter, true); 59 | 60 | t.end(); 61 | }); 62 | 63 | test('lookup fn should return name for react-router view render initialization', function(t) { 64 | 65 | var expressView = new ExpressView('/index', options); 66 | t.equal(expressView.lookup('SOME_STRING'), 'SOME_STRING'); 67 | 68 | t.end(); 69 | }); 70 | 71 | test('lookup fn should call original `lookup` function on View`s prototype for plain react views', function(t) { 72 | 73 | var expressView = new ExpressView('index', options); 74 | 75 | var spy = sinon.spy(View.prototype, 'lookup'); 76 | expressView.lookup('SOME_STRING'); 77 | t.equal(spy.lastCall.args[0], 'SOME_STRING'); 78 | 79 | View.prototype.lookup.restore(); 80 | t.end(); 81 | }); 82 | 83 | test('render fn should call our registered engine for react-router views', function(t) { 84 | 85 | var expressView = new ExpressView('/index', options); 86 | var spy = sinon.spy(expressView, 'engine'); 87 | 88 | var renderOptions = {}; 89 | var renderFn = function() {}; 90 | 91 | expressView.render(renderOptions, renderFn); 92 | 93 | t.equal(spy.lastCall.args[0], '/index'); 94 | t.equal(spy.lastCall.args[1], renderOptions); 95 | t.equal(spy.lastCall.args[2], renderFn); 96 | 97 | expressView.engine.restore(); 98 | t.end(); 99 | }); 100 | 101 | test('render fn should call original `render` function on View`s prototype for plain react views', function(t) { 102 | 103 | var expressView = new ExpressView('index', options); 104 | var renderOptions = {}; 105 | var renderFn = function() {}; 106 | 107 | var spy = sinon.spy(View.prototype, 'render'); 108 | expressView.render(renderOptions, renderFn); 109 | 110 | t.equal(spy.lastCall.args[0], renderOptions); 111 | t.equal(spy.lastCall.args[1], renderFn); 112 | 113 | View.prototype.render.restore(); 114 | t.end(); 115 | }); 116 | -------------------------------------------------------------------------------- /test/fixtures/assertions.json: -------------------------------------------------------------------------------- 1 | { 2 | "PROFILE_OUTPUT": "Hello, world!

Joshua

", 3 | "PROFILE_OUTPUT_CUSTOM_SCRIPT_TYPE": "Hello, world!

Joshua

", 4 | "PROFILE_OUTPUT_WITH_REACT_ATTRS": "Hello, world!

Joshua

", 5 | "PROFILE_OUTPUT_STATIC_MARKUP": "Hello, world!

Joshua

", 6 | "ACCOUNT_OUTPUT": "Hello, world!

Joshua

" 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/reactRoutes.jsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | 'use strict'; 17 | 18 | var React = require('react'); 19 | var Router = require('react-router'); 20 | 21 | var App = require('./views/app.jsx'); 22 | var Account = require('./views/account.jsx'); 23 | 24 | var routes = module.exports = ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | -------------------------------------------------------------------------------- /test/fixtures/views/account.jsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | 'use strict'; 17 | 18 | var React = require('react'); 19 | 20 | module.exports = (props) => { 21 | return ( 22 |
23 |

{props.name || 'Joshua'}

24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /test/fixtures/views/app.jsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | 'use strict'; 17 | 18 | var React = require('react'); 19 | var Layout = require('./layout.jsx'); 20 | 21 | module.exports = (props) => { 22 | return ( 23 | 24 | {props.children} 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /test/fixtures/views/layout.jsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | 'use strict'; 17 | 18 | var React = require('react'); 19 | 20 | module.exports = (props) => { 21 | return ( 22 | 23 | 24 | 25 | {props.title || 'Hello, world!'} 26 | 27 | 28 | {props.children} 29 | 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /test/fixtures/views/profile.jsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | 'use strict'; 17 | 18 | var React = require('react'); 19 | var Layout = require('./layout.jsx'); 20 | 21 | module.exports = (props) => { 22 | return ( 23 | 24 |
25 |

{props.name}

26 |
27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------------------------------------------------*\ 2 | | Copyright (C) 2017 PayPal | 3 | | | 4 | | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | 5 | | with the License. | 6 | | | 7 | | You may obtain a copy of the License at | 8 | | | 9 | | http://www.apache.org/licenses/LICENSE-2.0 | 10 | | | 11 | | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | 12 | | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | 13 | | the specific language governing permissions and limitations under the License. | 14 | \*-------------------------------------------------------------------------------------------------------------------*/ 15 | 16 | 'use strict'; 17 | 18 | require('babel-register')({ 19 | presets: ['react'] 20 | }); 21 | 22 | var fs = require('fs'); 23 | var path = require('path'); 24 | var test = require('tape'); 25 | var express = require('express'); 26 | var cheerio = require('cheerio'); 27 | var renderer = require('../index').server; 28 | var assertions = require('./fixtures/assertions.json'); 29 | 30 | var DATA_MODEL = exports.DATA_MODEL = { 31 | title: 'Hello, world!', 32 | name: 'Joshua', 33 | xss: '' 34 | }; 35 | 36 | // helpers 37 | function inject(path, callback) { 38 | var req = require('http').request({ method: 'GET', port: 8888, path: path }, function(res) { 39 | var data = []; 40 | 41 | res.on('data', function(chunk) { 42 | data.push(chunk); 43 | }); 44 | 45 | res.on('end', function() { 46 | var body = Buffer.concat(data).toString('utf8'); 47 | if (res.statusCode !== 200) { 48 | callback(new Error(body)); 49 | return; 50 | } 51 | 52 | callback(null, body); 53 | }); 54 | 55 | }); 56 | 57 | req.on('error', callback); 58 | req.end(); 59 | } 60 | 61 | function setup(options) { 62 | 63 | var setupEngine = options.engine; 64 | var expressRoutes = options.expressRoutes; 65 | var cb = options.onSetup; 66 | 67 | var app = express(); 68 | 69 | if (!expressRoutes) { 70 | expressRoutes = function(req, res) { 71 | res.render(req.path.substr(1), DATA_MODEL); 72 | }; 73 | } 74 | 75 | app.engine('jsx', setupEngine); 76 | app.set('view engine', 'jsx'); 77 | app.set('view cache', false); 78 | app.set('views', path.resolve(__dirname, 'fixtures/views')); 79 | 80 | app.set('view', require('../lib/expressView')); 81 | 82 | if (!!options.production) { 83 | app.set('env', 'production'); 84 | } 85 | 86 | app.get('/*', expressRoutes); 87 | 88 | var server = app.listen(8888, function() { 89 | cb(function(t) { 90 | server.once('close', function() { 91 | t.end(); 92 | }); 93 | 94 | server.close(); 95 | }); 96 | }); 97 | } 98 | 99 | function stripReactDataAttr($/*cheerio*/) { 100 | $('*').removeAttr('data-reactid'). 101 | removeAttr('data-react-checksum'). 102 | removeAttr('data-reactroot'); 103 | } 104 | 105 | /* 106 | ------------------------- 107 | start of test definitions 108 | ------------------------- 109 | */ 110 | 111 | test('react-engine public api', function(t) { 112 | var index = require('../index'); 113 | t.strictEqual(typeof index.server.create, 'function'); 114 | t.strictEqual(typeof index.client.data, 'function'); 115 | t.strictEqual(typeof index.client.boot, 'function'); 116 | t.strictEqual(typeof index.expressView, 'function'); 117 | t.strictEqual(typeof index.reactRouterServerErrors, 'object'); 118 | t.strictEqual(index.reactRouterServerErrors.MATCH_REDIRECT, 'MATCH_REDIRECT'); 119 | t.strictEqual(index.reactRouterServerErrors.MATCH_NOT_FOUND, 'MATCH_NOT_FOUND'); 120 | t.strictEqual(index.reactRouterServerErrors.MATCH_INTERNAL_ERROR, 'MATCH_INTERNAL_ERROR'); 121 | t.throws(function reactRouterServerErrorsObjectShouldNotBeModifiable() { 122 | index.reactRouterServerErrors.MATCH_REDIRECT = '123'; 123 | }); 124 | 125 | t.end(); 126 | }); 127 | 128 | test('construct an engine', function(t) { 129 | var engine = renderer.create(); 130 | t.ok(engine instanceof Function); 131 | t.end(); 132 | }); 133 | 134 | test('rendering a react view', function(t) { 135 | var options = { 136 | engine: renderer.create(), 137 | onSetup: function(done) { 138 | inject('/profile', function(err, data) { 139 | t.error(err); 140 | var $ = cheerio.load(data); 141 | stripReactDataAttr($); 142 | t.strictEqual($.html(), assertions.PROFILE_OUTPUT); 143 | done(t); 144 | }); 145 | } 146 | }; 147 | setup(options); 148 | }); 149 | 150 | test('rendering a react view with custom script type', function(t) { 151 | var options = { 152 | engine: renderer.create({scriptType: 'application/ld+json'}), 153 | onSetup: function(done) { 154 | inject('/profile', function(err, data) { 155 | t.error(err); 156 | var $ = cheerio.load(data); 157 | stripReactDataAttr($); 158 | t.strictEqual($.html(), assertions.PROFILE_OUTPUT_CUSTOM_SCRIPT_TYPE); 159 | done(t); 160 | }); 161 | } 162 | }; 163 | setup(options); 164 | }); 165 | 166 | test('rendering a react view to static markup', function(t) { 167 | var options = { 168 | engine: renderer.create({ staticMarkup: true }), 169 | onSetup: function(done) { 170 | inject('/profile', function(err, data) { 171 | t.error(err); 172 | var $ = cheerio.load(data); 173 | t.strictEqual($.html(), assertions.PROFILE_OUTPUT_STATIC_MARKUP); 174 | done(t); 175 | }); 176 | } 177 | }; 178 | setup(options); 179 | }); 180 | 181 | test('performance collector to be asserted to be a function', function(t) { 182 | 183 | function underTest1() { 184 | renderer.create({ 185 | performanceCollector: 'SOME_STRING_AND_NOT_FUNCTION' 186 | }); 187 | } 188 | 189 | function underTest2() { 190 | renderer.create({ 191 | performanceCollector: console.dir 192 | }); 193 | } 194 | 195 | t.throws(underTest1); 196 | t.doesNotThrow(underTest2); 197 | t.end(); 198 | }); 199 | 200 | test('performance collector', function(t) { 201 | 202 | var recorder = []; 203 | 204 | function collector(stats) { 205 | recorder.push(stats); 206 | } 207 | 208 | var options = { 209 | engine: renderer.create({ 210 | performanceCollector: collector 211 | }), 212 | onSetup: function(done) { 213 | inject('/profile', function(err, data) { 214 | t.error(err); 215 | t.strictEqual(typeof data, 'string'); 216 | t.strictEqual(recorder.length, 1); 217 | t.strictEqual(Object.keys(recorder[0]).length, 4); 218 | t.strictEqual(recorder[0].name, path.resolve(__dirname, 'fixtures/views', 'profile.jsx')); 219 | t.strictEqual(typeof recorder[0].startTime, 'number'); 220 | t.strictEqual(typeof recorder[0].endTime, 'number'); 221 | t.strictEqual(typeof recorder[0].duration, 'number'); 222 | t.ok(recorder[0].endTime > recorder[0].startTime); 223 | done(t); 224 | }); 225 | } 226 | }; 227 | setup(options); 228 | }); 229 | 230 | test('all views get cleared from require cache in dev mode', function(t) { 231 | var options = { 232 | engine: renderer.create(), 233 | onSetup: function(done) { 234 | inject('/profile', function(err) { 235 | t.error(err); 236 | var viewsDir = path.resolve(__dirname, 'fixtures/views'); 237 | var viewFiles = fs.readdirSync(viewsDir); 238 | viewFiles.map(function(file) { 239 | var view = path.resolve(viewsDir, file); 240 | t.strictEqual(require.cache[view], undefined); 241 | }); 242 | 243 | done(t); 244 | }); 245 | } 246 | }; 247 | setup(options); 248 | }); 249 | 250 | test('all views get cleared from require cache ONLY in dev mode', function(t) { 251 | var options = { 252 | production: true, 253 | engine: renderer.create(), 254 | onSetup: function(done) { 255 | inject('/profile', function(err) { 256 | t.error(err); 257 | var viewsDir = path.resolve(__dirname, 'fixtures/views'); 258 | var viewFiles = fs.readdirSync(viewsDir); 259 | viewFiles.some(function(file) { 260 | var view = path.resolve(viewsDir, file); 261 | return require.cache[view] !== undefined; 262 | }); 263 | 264 | t.notEqual(viewFiles.length, 0); 265 | done(t); 266 | }); 267 | } 268 | }; 269 | setup(options); 270 | }); 271 | 272 | test('router gets run when we pass urls into render function', function(t) { 273 | 274 | var options = { 275 | engine: renderer.create({ 276 | routes: require(path.join(__dirname + '/fixtures/reactRoutes.jsx')) 277 | }), 278 | expressRoutes: function(req, res) { 279 | res.render(req.url, DATA_MODEL); 280 | }, 281 | 282 | onSetup: function(done) { 283 | inject('/account', function(err, data) { 284 | t.error(err); 285 | var $ = cheerio.load(data); 286 | stripReactDataAttr($); 287 | t.strictEqual($.html(), assertions.ACCOUNT_OUTPUT); 288 | done(t); 289 | }); 290 | } 291 | }; 292 | setup(options); 293 | }); 294 | 295 | test('error that renderer throws when asked to run react router without providing a react-router route', function(t) { 296 | 297 | var options = { 298 | engine: renderer.create(), 299 | expressRoutes: function(req, res) { 300 | res.render(req.url, DATA_MODEL); 301 | }, 302 | 303 | onSetup: function(done) { 304 | inject('/account', function(err, data) { 305 | var errorMessage = err.message; 306 | var matchIndex = errorMessage.indexOf('asking to use react router for rendering, but no routes are provided'); 307 | t.notEqual(matchIndex, -1); 308 | t.ok(typeof data === 'undefined'); 309 | done(t); 310 | }); 311 | } 312 | }; 313 | setup(options); 314 | }); 315 | 316 | test('all keys in express render `options` should be be sent to client', function(t) { 317 | 318 | var options = { 319 | engine: renderer.create({ 320 | routes: require(path.join(__dirname + '/fixtures/reactRoutes.jsx')) 321 | }), 322 | expressRoutes: function(req, res) { 323 | res.locals.someSensitiveData = 1234; 324 | res.render(req.url, DATA_MODEL); 325 | }, 326 | 327 | onSetup: function(done) { 328 | inject('/account', function(err, data) { 329 | t.error(err); 330 | var $ = cheerio.load(data); 331 | var matchIndex = $.html().indexOf('someSensitiveData'); 332 | t.notEqual(matchIndex, -1); 333 | done(t); 334 | }); 335 | } 336 | }; 337 | setup(options); 338 | }); 339 | 340 | test('all keys in express render `renderOptionsKeysToFilter` should be used to filter out renderOptions', function(t) { 341 | 342 | var options = { 343 | engine: renderer.create({ 344 | routes: require(path.join(__dirname + '/fixtures/reactRoutes.jsx')), 345 | renderOptionsKeysToFilter: ['someSensitiveData'] 346 | }), 347 | expressRoutes: function(req, res) { 348 | res.locals.someSensitiveData = 1234; 349 | res.render(req.url, DATA_MODEL); 350 | }, 351 | 352 | onSetup: function(done) { 353 | inject('/account', function(err, data) { 354 | t.error(err); 355 | var $ = cheerio.load(data); 356 | var matchIndex = $.html().indexOf('someSensitiveData'); 357 | t.equal(matchIndex, -1); 358 | done(t); 359 | }); 360 | } 361 | }; 362 | setup(options); 363 | }); 364 | 365 | test('deep keys in express render `renderOptionsKeysToFilter` should be used to filter out nested renderOptions', function(t) { 366 | 367 | var options = { 368 | engine: renderer.create({ 369 | routes: require(path.join(__dirname + '/fixtures/reactRoutes.jsx')), 370 | renderOptionsKeysToFilter: ['someSensitiveData.omitDeepProp'] 371 | }), 372 | expressRoutes: function(req, res) { 373 | res.locals.someSensitiveData = { passDeepPropThrough: 1234, omitDeepProp: 5678 }; 374 | res.render(req.url, DATA_MODEL); 375 | }, 376 | 377 | onSetup: function(done) { 378 | inject('/account', function(err, data) { 379 | t.error(err); 380 | var html = cheerio.load(data).html(); 381 | function present(str) { 382 | t.notEqual(html.indexOf(str), -1, str + ' was not present in render'); 383 | } 384 | function absent(str) { 385 | t.equal(html.indexOf(str), -1, str + ' was not removed from render'); 386 | } 387 | present('someSensitiveData'); 388 | present('passDeepPropThrough'); 389 | absent('omitDeepProp'); 390 | done(t); 391 | }); 392 | } 393 | }; 394 | setup(options); 395 | }); 396 | 397 | test('error that renderer throws when asked to run a unknown route', function(t) { 398 | 399 | var options = { 400 | engine: renderer.create({ 401 | routes: require(path.join(__dirname + '/fixtures/reactRoutes.jsx')) 402 | }), 403 | expressRoutes: function(req, res) { 404 | res.render(req.url, DATA_MODEL); 405 | }, 406 | 407 | onSetup: function(done) { 408 | inject('/some_garbage', function(err, data) { 409 | // TODO: t.strictEqual(err._type, 'MATCH_NOT_FOUND'); 410 | t.ok(typeof err === 'object'); 411 | t.ok(typeof data === 'undefined'); 412 | done(t); 413 | }); 414 | } 415 | }; 416 | setup(options); 417 | }); 418 | 419 | test('error that renderer throws when asked to run a redirect route', function(t) { 420 | 421 | var options = { 422 | engine: renderer.create({ 423 | routes: require(path.join(__dirname + '/fixtures/reactRoutes.jsx')) 424 | }), 425 | expressRoutes: function(req, res) { 426 | res.render(req.url, DATA_MODEL); 427 | }, 428 | 429 | onSetup: function(done) { 430 | inject('/gohome', function(err, data) { 431 | // TODO: t.strictEqual(err._type, 'MATCH_REDIRECT'); 432 | t.ok(typeof err === 'object'); 433 | t.ok(typeof data === 'undefined'); 434 | done(t); 435 | }); 436 | } 437 | }; 438 | setup(options); 439 | }); 440 | -------------------------------------------------------------------------------- /upgrade-guide.md: -------------------------------------------------------------------------------- 1 | ### Migration from 3.x to 4.x 2 | 4.x version of `react-engine` removes the dependency of `react-dom` from the project. Users of `react-engine` should install `react-dom` along side `react` going forward. 3 | 4 | ### Migration from 2.x to 3.x 5 | While upgrading to 3.x version of react-engine, make sure to follow the [react-router's 2.x upgrade guide](https://github.com/reactjs/react-router/blob/master/upgrade-guides/v2.0.0.md) to upgrade react-router related code in your app. 6 | Then, add to your express error middleware, react-engine's MATCH_REDIRECT and MATCH_NOT_FOUND checks. 7 | 8 | ### Migration from 1.x to 2.x 9 | 2.x version of react-engine brought in a major api change. Basically it affects the property names of the [object that gets passed in during the engine creation](https://github.com/paypal/react-engine#server-options-spec) on the server side and also how routes definition is passed into react-engine. 10 | 11 | In v2.x, `routes` need to be explicitly required and passed in to the engine creation method. Also, any [react-router known properties can be passed in](https://github.com/reactjs/react-router/blob/0.13.x/doc/02%20Top-Level/Router.create.md). 12 | 13 | An example engine creation can be found [here](https://github.com/paypal/react-engine/blob/71ac27196e72059484332a491cd66982797a60a3/examples/complex/index.js#L28). 14 | --------------------------------------------------------------------------------