From ed8076b376982e341794ea85629e41c5e2d01542 Mon Sep 17 00:00:00 2001 From: "Selvanathan, Sam" Date: Mon, 5 Oct 2015 19:58:18 -0700 Subject: [PATCH 01/28] attempt #1 to make react-engine compatible with react-router v1.x --- lib/server.js | 23 +++++++++++++++-------- package.json | 12 ++++-------- prune_peer_deps.sh | 10 ---------- test/fixtures/reactRoutes.js | 4 ++-- 4 files changed, 21 insertions(+), 28 deletions(-) delete mode 100755 prune_peer_deps.sh diff --git a/lib/server.js b/lib/server.js index 00beeea..235c260 100644 --- a/lib/server.js +++ b/lib/server.js @@ -19,6 +19,7 @@ var path = require('path'); var util = require('./util'); var assert = require('assert'); var Config = require('./config'); +var ReactDOM = require('react-dom/server'); var format = require('util').format; var Performance = require('./performance'); var omit = require('lodash-node/compat/object/omit'); @@ -28,6 +29,9 @@ var merge = require('lodash-node/compat/object/merge'); var React = util.safeRequire('react'); var Router = util.safeRequire('react-router'); +var match = Router.match; +var RoutingContext = Router.RoutingContext; + // a template of the `script` tag that gets // injected into the server rendered pages. var TEMPLATE = ['", - "PROFILE_OUTPUT_WITH_REACT_ATTRS": "Hello, world!

Joshua

", + "PROFILE_OUTPUT_WITH_REACT_ATTRS": "Hello, world!

Joshua

", "ACCOUNT_OUTPUT": "Hello, world!

Joshua

" } diff --git a/test/fixtures/reactRoutes.js b/test/fixtures/reactRoutes.js index 9906955..b2162c8 100644 --- a/test/fixtures/reactRoutes.js +++ b/test/fixtures/reactRoutes.js @@ -22,11 +22,11 @@ var App = require('./views/app'); var Account = require('./views/account'); module.exports = React.createElement( + Router, + null, + React.createElement( Router.Route, - { path: '/' }, - React.createElement( - Router.Route, - { path: 'account', component: App }, - React.createElement(Router.IndexRoute, { component: Account }) - ) + { path: '/', component: App }, + React.createElement(Router.Route, { path: '/account', component: Account }) + ) ); diff --git a/test/fixtures/views/account.js b/test/fixtures/views/account.js index fd4be86..d1c6073 100644 --- a/test/fixtures/views/account.js +++ b/test/fixtures/views/account.js @@ -21,15 +21,15 @@ module.exports = React.createClass({ displayName: 'account', - render: function render() { - + render: function render(props) { + var name = this.props.name || 'Joshua'; return React.createElement( 'div', { id: 'account' }, React.createElement( 'h1', null, - this.props.name + name ) ); } diff --git a/test/fixtures/views/app.js b/test/fixtures/views/app.js index 446e137..3ec1ce6 100644 --- a/test/fixtures/views/app.js +++ b/test/fixtures/views/app.js @@ -26,7 +26,7 @@ module.exports = React.createClass({ render: function render() { return React.createElement(Layout, this.props, - React.createElement(Router.RouteHandler, this.props) + React.cloneElement(this.props.children, this.props) ); } }); diff --git a/test/fixtures/views/layout.js b/test/fixtures/views/layout.js index 59387a0..6e6635b 100644 --- a/test/fixtures/views/layout.js +++ b/test/fixtures/views/layout.js @@ -22,13 +22,13 @@ module.exports = React.createClass({ displayName: 'layout', render: function render() { - + var title = this.props.title || 'Hello, world!'; return React.createElement( 'html', null, React.createElement( 'head', null, React.createElement('meta', { charSet: 'utf-8' }), - React.createElement('title', null, this.props.title) + React.createElement('title', null, title) ), React.createElement('body', null, this.props.children) ); From a61dd2f7f77c81e950dd61ed191bcace1a8e5a1f Mon Sep 17 00:00:00 2001 From: Benjamin Goh Date: Tue, 13 Oct 2015 21:34:22 -0700 Subject: [PATCH 03/28] Multiple bug fixes + client-side redirect * Adding edge example to do live testing * Working on tests * Deal with JSCS issues --- .gitignore | 5 +- .jscsrc | 1 + examples/edge/.eslintignore | 1 + examples/edge/.eslintrc | 17 ++++++ examples/edge/README.md | 11 ++++ examples/edge/index.js | 72 +++++++++++++++++++++++++ examples/edge/package.json | 21 ++++++++ examples/edge/public/index.js | 40 ++++++++++++++ examples/edge/public/routes.jsx | 29 ++++++++++ examples/edge/public/views/404.jsx | 30 +++++++++++ examples/edge/public/views/account.jsx | 34 ++++++++++++ examples/edge/public/views/layout.jsx | 37 +++++++++++++ examples/edge/public/views/messages.jsx | 49 +++++++++++++++++ examples/simple/package.json | 4 +- lib/client.js | 16 ++---- lib/server.js | 39 ++++++++------ 16 files changed, 373 insertions(+), 33 deletions(-) create mode 100644 examples/edge/.eslintignore create mode 100644 examples/edge/.eslintrc create mode 100644 examples/edge/README.md create mode 100644 examples/edge/index.js create mode 100644 examples/edge/package.json create mode 100644 examples/edge/public/index.js create mode 100644 examples/edge/public/routes.jsx create mode 100644 examples/edge/public/views/404.jsx create mode 100644 examples/edge/public/views/account.jsx create mode 100644 examples/edge/public/views/layout.jsx create mode 100644 examples/edge/public/views/messages.jsx diff --git a/.gitignore b/.gitignore index d6ab22b..b4217d0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,5 @@ node_modules/ logs *.log coverage/ -examples/simple/public/bundle.js -examples/complex/public/bundle.js -examples/webpack/public/bundle.js +examples/**/node_modules +examples/**/public/bundle.js \ No newline at end of file diff --git a/.jscsrc b/.jscsrc index 6ba2b1c..3f6f5f3 100644 --- a/.jscsrc +++ b/.jscsrc @@ -6,6 +6,7 @@ "./examples/simple/node_modules/**", "./examples/complex/node_modules/**", "./examples/webpack/node_modules/**", + "./examples/edge/**", "./examples/simple/public/bundle.js", "./examples/complex/public/bundle.js", "./examples/webpack/public/bundle.js"] diff --git a/examples/edge/.eslintignore b/examples/edge/.eslintignore new file mode 100644 index 0000000..7f3dd62 --- /dev/null +++ b/examples/edge/.eslintignore @@ -0,0 +1 @@ +public/bundle.js diff --git a/examples/edge/.eslintrc b/examples/edge/.eslintrc new file mode 100644 index 0000000..36b1ed7 --- /dev/null +++ b/examples/edge/.eslintrc @@ -0,0 +1,17 @@ +{ + "rules": { + "no-undef": 2, + "no-unused-vars": [2, {"vars": "all", "args": "after-used", "varsIgnorePattern": "React"}] + }, + "env": { + "node": true, + "es6": true + }, + "ecmaFeatures": { + "modules": true, + "jsx": true + }, + "globals": { + "React": true + } +} diff --git a/examples/edge/README.md b/examples/edge/README.md new file mode 100644 index 0000000..0960deb --- /dev/null +++ b/examples/edge/README.md @@ -0,0 +1,11 @@ +#### a complex express app to demonstrate the usage of react-engine + +##### Uses react views and react-router views + +###### to run +```shell +cd `into_this_dir` +npm install +npm start +open http://localhost:3000 +``` diff --git a/examples/edge/index.js b/examples/edge/index.js new file mode 100644 index 0000000..69721e4 --- /dev/null +++ b/examples/edge/index.js @@ -0,0 +1,72 @@ +/*-------------------------------------------------------------------------------------------------------------------*\ +| Copyright (C) 2015 PayPal | +| | +| Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | +| with the License. | +| | +| You may obtain a copy of the License at | +| | +| http://www.apache.org/licenses/LICENSE-2.0 | +| | +| Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | +| on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | +| the specific language governing permissions and limitations under the License. | +\*-------------------------------------------------------------------------------------------------------------------*/ + +'use strict'; + +// make `.jsx` file requirable by node +require('babel/register'); + +var path = require('path'); +var express = require('express'); +var renderer = require('react-engine'); + +var app = express(); + +// create the view engine with `react-engine` +var engine = renderer.server.create({ + routes: require(path.join(__dirname, '/public/routes.jsx')), + routesFilePath: path.join(__dirname, '/public/routes.jsx') +}); + +// set the engine +app.engine('.jsx', engine); + +// set the view directory +app.set('views', path.join(__dirname, '/public/views')); + +// set jsx as the view engine +app.set('view engine', 'jsx'); + +// finally, set the custom view +app.set('view', renderer.expressView); + +//expose public folder as static assets +app.use(express.static(path.join(__dirname, '/public'))); + +// match everything and work from there +app.use('/', function(req, res) { + if (req.originalUrl !== '/favicon.ico') { + res.render(req.originalUrl, { + title: 'React Engine Express Sample App', + name: 'Jordan' + }); + } +}); + +// 404 template +app.use(function(req, res) { + res.render('404', { + title: 'React Engine Express Sample App', + url: req.url + }); +}); + +var server = app.listen(3000, function() { + + var host = server.address().address; + var port = server.address().port; + + console.log('Example app listening at http://%s:%s', host, port); +}); diff --git a/examples/edge/package.json b/examples/edge/package.json new file mode 100644 index 0000000..e1c7a09 --- /dev/null +++ b/examples/edge/package.json @@ -0,0 +1,21 @@ +{ + "name": "react-engine-example-edge", + "main": "index.js", + "scripts": { + "start": "npm run browserify && node index.js", + "browserify": "browserify -t babelify -t require-globify public/index.js -o public/bundle.js" + }, + "dependencies": { + "babel": "^5.8.23", + "browserify": "^9.0.4", + "express": "^4.12.3", + "react": "^0.14", + "react-engine": "../../", + "react-router": "1.0.0-rc3", + "reactify": "^1.1.0", + "require-globify": "^1.1.0" + }, + "devDependencies": { + "babelify": "^6.3.0" + } +} diff --git a/examples/edge/public/index.js b/examples/edge/public/index.js new file mode 100644 index 0000000..9afcff9 --- /dev/null +++ b/examples/edge/public/index.js @@ -0,0 +1,40 @@ +/*-------------------------------------------------------------------------------------------------------------------*\ +| Copyright (C) 2015 PayPal | +| | +| Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | +| with the License. | +| | +| You may obtain a copy of the License at | +| | +| http://www.apache.org/licenses/LICENSE-2.0 | +| | +| Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | +| on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | +| the specific language governing permissions and limitations under the License. | +\*-------------------------------------------------------------------------------------------------------------------*/ +/* global document */ + +'use strict'; + +var Routes = require('./routes.jsx'); +var Client = require('react-engine/lib/client'); + +// Include all view files. Browerify doesn't do +// this automatically as it can only operate on +// static require statements. +require('./views/**/*.jsx', {glob: true}); + +// boot options +var options = { + routes: Routes, + + // supply a function that can be called + // to resolve the file that was rendered. + viewResolver: function(viewName) { + return require('./views/' + viewName); + } +}; + +document.addEventListener('DOMContentLoaded', function onLoad() { + Client.boot(options); +}); diff --git a/examples/edge/public/routes.jsx b/examples/edge/public/routes.jsx new file mode 100644 index 0000000..3a87de6 --- /dev/null +++ b/examples/edge/public/routes.jsx @@ -0,0 +1,29 @@ +/*-------------------------------------------------------------------------------------------------------------------*\ +| Copyright (C) 2015 PayPal | +| | +| Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | +| with the License. | +| | +| You may obtain a copy of the License at | +| | +| http://www.apache.org/licenses/LICENSE-2.0 | +| | +| Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | +| on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | +| the specific language governing permissions and limitations under the License. | +\*-------------------------------------------------------------------------------------------------------------------*/ + +import React from 'react'; +import { Route, IndexRoute, Redirect } from 'react-router'; +import Layout from './views/layout.jsx'; +import Account from './views/account.jsx'; +import Messages from './views/messages.jsx'; + +var routes = module.exports = ( + + + + + + +); \ No newline at end of file diff --git a/examples/edge/public/views/404.jsx b/examples/edge/public/views/404.jsx new file mode 100644 index 0000000..0dfce27 --- /dev/null +++ b/examples/edge/public/views/404.jsx @@ -0,0 +1,30 @@ +/*-------------------------------------------------------------------------------------------------------------------*\ +| Copyright (C) 2015 PayPal | +| | +| Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | +| with the License. | +| | +| You may obtain a copy of the License at | +| | +| http://www.apache.org/licenses/LICENSE-2.0 | +| | +| Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | +| on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | +| the specific language governing permissions and limitations under the License. | +\*-------------------------------------------------------------------------------------------------------------------*/ + +import Layout from './layout.jsx'; +import React from 'react'; + +export default React.createClass({ + + render() { + + return ( + +

URL: {this.props.url} - Not Found(404)

+
I am a Plain vanilla react view
+
+ ); + } +}); diff --git a/examples/edge/public/views/account.jsx b/examples/edge/public/views/account.jsx new file mode 100644 index 0000000..333448f --- /dev/null +++ b/examples/edge/public/views/account.jsx @@ -0,0 +1,34 @@ +/*-------------------------------------------------------------------------------------------------------------------*\ +| Copyright (C) 2015 PayPal | +| | +| Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | +| with the License. | +| | +| You may obtain a copy of the License at | +| | +| http://www.apache.org/licenses/LICENSE-2.0 | +| | +| Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | +| on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | +| the specific language governing permissions and limitations under the License. | +\*-------------------------------------------------------------------------------------------------------------------*/ + +import React from 'react'; + +export default React.createClass({ + + displayName: 'account', + + render() { + + return ( +
+

{this.props.name}

+
I am a React Router rendered view
+ Click to go to an unhandled route + Messages + Redirects to /messages +
+ ); + } +}); diff --git a/examples/edge/public/views/layout.jsx b/examples/edge/public/views/layout.jsx new file mode 100644 index 0000000..8ccacf1 --- /dev/null +++ b/examples/edge/public/views/layout.jsx @@ -0,0 +1,37 @@ +/*-------------------------------------------------------------------------------------------------------------------*\ +| Copyright (C) 2015 PayPal | +| | +| Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | +| with the License. | +| | +| You may obtain a copy of the License at | +| | +| http://www.apache.org/licenses/LICENSE-2.0 | +| | +| Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | +| on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | +| the specific language governing permissions and limitations under the License. | +\*-------------------------------------------------------------------------------------------------------------------*/ + +import React from 'react'; + +export default React.createClass({ + + render() { + + return ( + + + + + {this.props.title} + + + + {this.props.children} + + + + ); + } +}); diff --git a/examples/edge/public/views/messages.jsx b/examples/edge/public/views/messages.jsx new file mode 100644 index 0000000..836e837 --- /dev/null +++ b/examples/edge/public/views/messages.jsx @@ -0,0 +1,49 @@ +/*-------------------------------------------------------------------------------------------------------------------*\ +| Copyright (C) 2015 PayPal | +| | +| Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | +| with the License. | +| | +| You may obtain a copy of the License at | +| | +| http://www.apache.org/licenses/LICENSE-2.0 | +| | +| Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | +| on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | +| the specific language governing permissions and limitations under the License. | +\*-------------------------------------------------------------------------------------------------------------------*/ + +import React from 'react'; + +export default React.createClass({ + + displayName: 'messages', + + getDefaultProps() { + return { + name: 'Messages', + messages: [ + { id: 1, text: 'Lorem' }, + { id: 2, text: 'Ipsum' }, + { id: 3, text: 'Dolor' } + ] + } + }, + + render() { + + return ( +
+

{this.props.name}

+
    + { + this.props.messages.map( message =>
  1. {message.text}
  2. ) + } +
+ Click to go to an unhandled route + Messages + Redirects to /messages +
+ ); + } +}); diff --git a/examples/simple/package.json b/examples/simple/package.json index 4d6639a..c86c08c 100644 --- a/examples/simple/package.json +++ b/examples/simple/package.json @@ -9,8 +9,8 @@ "babel": "^5.8.23", "browserify": "^9.0.4", "express": "^4.12.3", - "react": "^0.12.2", - "react-engine": "^1.3.0", + "react": "^0.14.0-rc1", + "react-engine": "../../", "react-router": "^0.12.4", "reactify": "^1.1.0", "require-globify": "^1.1.0" diff --git a/lib/client.js b/lib/client.js index 6ce6f6b..a321907 100644 --- a/lib/client.js +++ b/lib/client.js @@ -18,6 +18,7 @@ var React = require('react'); var Config = require('./config'); var Router = require('react-router'); +var history = require('history'); var ReactDOM = require('react-dom'); // declaring like this helps in unit test @@ -51,19 +52,8 @@ exports.boot = function boot(options, callback) { throw new Error('asking to use react router for rendering, but no routes are provided'); } - // seems pointless to do this. - options.location = options.location || Router.HistoryLocation; - - // create and run the router - var router = Router.create(options); - - router.run(function onRouterRun(Component) { - // create a component instance - var componentInstance = React.createElement(Component, props); - - // finally, render the component instance into the document - ReactDOM.render(componentInstance, _document); - }); + var routerComponent = React.createElement(Router.Router, { routes: options.routes, history: history.createHistory() }); + ReactDOM.render(routerComponent, _document); } else { // get the file from viewResolver supplying it with a view name var view = viewResolver(props.__meta.view); diff --git a/lib/server.js b/lib/server.js index 104b425..c764f5a 100644 --- a/lib/server.js +++ b/lib/server.js @@ -39,6 +39,7 @@ var TEMPLATE = ['' ].join(''); +var TEMPLATE_REDIRECT = ''; exports.create = function create(createOptions) { @@ -78,9 +79,21 @@ exports.create = function create(createOptions) { createOptions.performanceCollector(perfInstance()); } + console.log('done with %s', html); callback(err, html); } + function renderAndDecorate(component, html) { + // render the component + html += ReactDOMServer.renderToString(component); + + // state (script) injection + var script = format(TEMPLATE, Config.client.markupId, JSON.stringify(data)); + html = html.replace('', script + ''); + + return html; + } + if (createOptions.routes && createOptions.routesFilePath) { // if `routesFilePath` property is provided, then in // cases where 'view cache' is false, the routes are reloaded for every render. @@ -104,26 +117,31 @@ exports.create = function create(createOptions) { return done(new Error('asking to use react router for rendering, but no routes are provided')); } - var componentInstance; + var isRedirect = false; try { if (this.useRouter) { - match({ routes:createOptions.routes, location:thing}, + return match({ routes:createOptions.routes, location:thing}, function(error, redirectLocation, renderProps) { if (redirectLocation) { debug('server.js match 302 %s', redirectLocation); + // perform a client-side redirect, see https://github.com/rackt/history/blob/master/docs/Location.md + console.log('The redirectLocation is', require('util').inspect(redirectLocation)); + return done(null, format(TEMPLATE_REDIRECT, redirectLocation.pathname + redirectLocation.search)); + //res.redirect(302, redirectLocation.pathname + redirectLocation.search); } else if (error) { debug('server.js match 500 %s', error); //res.send(500, error.message); - } else if (renderProps === null) { - debug('server.js match 404 %s', renderProps); + } else if (renderProps) { + return done(null, renderAndDecorate(React.createElement(RoutingContext, renderProps), html)); //res.send(404, 'Not found'); } else { - componentInstance = React.createElement(RoutingContext, renderProps); + console.log('hit 404'); + debug('server.js match 404 %s', renderProps); } }); } else { @@ -141,17 +159,8 @@ exports.create = function create(createOptions) { // create the Component using react's createFactory var component = React.createFactory(view); - componentInstance = component(data); + return done(null, renderAndDecorate(component(data), html)); } - - // render the componentInstance - html += ReactDOMServer.renderToString(componentInstance); - - // state (script) injection - var script = format(TEMPLATE, Config.client.markupId, JSON.stringify(data)); - html = html.replace('', script + ''); - - return done(null, html); } catch (err) { From 3c7e08ec9089c623103b5e92acca206b9b51d5e1 Mon Sep 17 00:00:00 2001 From: Benjamin Goh Date: Wed, 14 Oct 2015 09:18:58 -0700 Subject: [PATCH 04/28] Remove assorted console.log --- lib/server.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/server.js b/lib/server.js index c764f5a..59cc6be 100644 --- a/lib/server.js +++ b/lib/server.js @@ -79,7 +79,6 @@ exports.create = function create(createOptions) { createOptions.performanceCollector(perfInstance()); } - console.log('done with %s', html); callback(err, html); } @@ -127,7 +126,6 @@ exports.create = function create(createOptions) { debug('server.js match 302 %s', redirectLocation); // perform a client-side redirect, see https://github.com/rackt/history/blob/master/docs/Location.md - console.log('The redirectLocation is', require('util').inspect(redirectLocation)); return done(null, format(TEMPLATE_REDIRECT, redirectLocation.pathname + redirectLocation.search)); //res.redirect(302, redirectLocation.pathname + redirectLocation.search); @@ -140,7 +138,6 @@ exports.create = function create(createOptions) { //res.send(404, 'Not found'); } else { - console.log('hit 404'); debug('server.js match 404 %s', renderProps); } }); From c9a91d612bafb41de5b8af9c238074614fb6dddb Mon Sep 17 00:00:00 2001 From: Benjamin Goh Date: Wed, 14 Oct 2015 09:27:07 -0700 Subject: [PATCH 05/28] Remove reactify in edge example --- examples/edge/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/edge/package.json b/examples/edge/package.json index e1c7a09..32634b3 100644 --- a/examples/edge/package.json +++ b/examples/edge/package.json @@ -12,7 +12,6 @@ "react": "^0.14", "react-engine": "../../", "react-router": "1.0.0-rc3", - "reactify": "^1.1.0", "require-globify": "^1.1.0" }, "devDependencies": { From f459a24a716eaccb31e3e088dc94d73e7c8a8147 Mon Sep 17 00:00:00 2001 From: Benjamin Goh Date: Wed, 14 Oct 2015 09:39:55 -0700 Subject: [PATCH 06/28] Remove erroneous modification to simple example --- examples/simple/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/simple/package.json b/examples/simple/package.json index c86c08c..4d6639a 100644 --- a/examples/simple/package.json +++ b/examples/simple/package.json @@ -9,8 +9,8 @@ "babel": "^5.8.23", "browserify": "^9.0.4", "express": "^4.12.3", - "react": "^0.14.0-rc1", - "react-engine": "../../", + "react": "^0.12.2", + "react-engine": "^1.3.0", "react-router": "^0.12.4", "reactify": "^1.1.0", "require-globify": "^1.1.0" From 0fcb864f4533616a245e22788f630c2d9f3c63ca Mon Sep 17 00:00:00 2001 From: Benjamin Goh Date: Wed, 14 Oct 2015 15:27:53 -0700 Subject: [PATCH 07/28] Fix annoying linting issues --- lib/client.js | 7 +++++-- lib/server.js | 2 -- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/client.js b/lib/client.js index a321907..0da72db 100644 --- a/lib/client.js +++ b/lib/client.js @@ -18,7 +18,7 @@ var React = require('react'); var Config = require('./config'); var Router = require('react-router'); -var history = require('history'); +var routerHistory = require('history'); var ReactDOM = require('react-dom'); // declaring like this helps in unit test @@ -52,7 +52,10 @@ exports.boot = function boot(options, callback) { throw new Error('asking to use react router for rendering, but no routes are provided'); } - var routerComponent = React.createElement(Router.Router, { routes: options.routes, history: history.createHistory() }); + var routerComponent = React.createElement(Router.Router, { + routes: options.routes, + history: routerHistory.createHistory() + }); ReactDOM.render(routerComponent, _document); } else { // get the file from viewResolver supplying it with a view name diff --git a/lib/server.js b/lib/server.js index 59cc6be..ad87eb9 100644 --- a/lib/server.js +++ b/lib/server.js @@ -116,8 +116,6 @@ exports.create = function create(createOptions) { return done(new Error('asking to use react router for rendering, but no routes are provided')); } - var isRedirect = false; - try { if (this.useRouter) { return match({ routes:createOptions.routes, location:thing}, From 20b3a66a5aa14f584688daa12987c0c4cf7e4b0b Mon Sep 17 00:00:00 2001 From: Benjamin Goh Date: Wed, 14 Oct 2015 17:30:27 -0700 Subject: [PATCH 08/28] Make data not rely on a global --- lib/server.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/server.js b/lib/server.js index ad87eb9..512fd55 100644 --- a/lib/server.js +++ b/lib/server.js @@ -82,7 +82,7 @@ exports.create = function create(createOptions) { callback(err, html); } - function renderAndDecorate(component, html) { + function renderAndDecorate(component, data, html) { // render the component html += ReactDOMServer.renderToString(component); @@ -132,7 +132,7 @@ exports.create = function create(createOptions) { //res.send(500, error.message); } else if (renderProps) { - return done(null, renderAndDecorate(React.createElement(RoutingContext, renderProps), html)); + return done(null, renderAndDecorate(React.createElement(RoutingContext, renderProps), data, html)); //res.send(404, 'Not found'); } else { @@ -154,7 +154,7 @@ exports.create = function create(createOptions) { // create the Component using react's createFactory var component = React.createFactory(view); - return done(null, renderAndDecorate(component(data), html)); + return done(null, renderAndDecorate(component(data), data, html)); } } catch (err) { From 13696847db3ae143d699bae9ef3e8f0ba828acd1 Mon Sep 17 00:00:00 2001 From: Benjamin Goh Date: Wed, 14 Oct 2015 18:34:31 -0700 Subject: [PATCH 09/28] Added props transferrence functionality --- examples/edge/index.js | 24 +++++++++++++++++++----- examples/edge/public/views/messages.jsx | 11 ----------- lib/client.js | 7 +++++++ lib/server.js | 13 +++++++++++-- package.json | 3 ++- 5 files changed, 39 insertions(+), 19 deletions(-) diff --git a/examples/edge/index.js b/examples/edge/index.js index 69721e4..a3d6034 100644 --- a/examples/edge/index.js +++ b/examples/edge/index.js @@ -46,13 +46,27 @@ app.set('view', renderer.expressView); app.use(express.static(path.join(__dirname, '/public'))); // match everything and work from there -app.use('/', function(req, res) { +app.use('/', function(req, res, next) { + var model = { + title: 'React Engine Express Sample App', + name: 'Jordan' + }; + + if (req.originalUrl === '/messages') { + model.name = 'Messages'; + model.messages = [ + {id: 1, text: 'Lorem'}, + {id: 2, text: 'Ipsum'}, + {id: 3, text: 'Dolor'} + ]; + } + + // hack to ignore favicon.ico if (req.originalUrl !== '/favicon.ico') { - res.render(req.originalUrl, { - title: 'React Engine Express Sample App', - name: 'Jordan' - }); + return res.render(req.originalUrl, model); } + + next(); }); // 404 template diff --git a/examples/edge/public/views/messages.jsx b/examples/edge/public/views/messages.jsx index 836e837..5c59383 100644 --- a/examples/edge/public/views/messages.jsx +++ b/examples/edge/public/views/messages.jsx @@ -19,17 +19,6 @@ export default React.createClass({ displayName: 'messages', - getDefaultProps() { - return { - name: 'Messages', - messages: [ - { id: 1, text: 'Lorem' }, - { id: 2, text: 'Ipsum' }, - { id: 3, text: 'Dolor' } - ] - } - }, - render() { return ( diff --git a/lib/client.js b/lib/client.js index 0da72db..72ad30a 100644 --- a/lib/client.js +++ b/lib/client.js @@ -20,6 +20,7 @@ var Config = require('./config'); var Router = require('react-router'); var routerHistory = require('history'); var ReactDOM = require('react-dom'); +var merge = require('lodash/object/merge'); // declaring like this helps in unit test // dependency injection using `rewire` module @@ -52,7 +53,13 @@ exports.boot = function boot(options, callback) { throw new Error('asking to use react router for rendering, but no routes are provided'); } + // for any component created by react-router, merge model data with the routerProps + // NOTE: This may be imposing too large of an opinion? var routerComponent = React.createElement(Router.Router, { + createElement: function(Component, routerProps) { + return React.createElement(Component, merge({}, props, routerProps)); + }, + routes: options.routes, history: routerHistory.createHistory() }); diff --git a/lib/server.js b/lib/server.js index 512fd55..d84c8d7 100644 --- a/lib/server.js +++ b/lib/server.js @@ -23,8 +23,8 @@ var ReactDOMServer = require('react-dom/server'); var format = require('util').format; var Performance = require('./performance'); -var omit = require('lodash-node/compat/object/omit'); -var merge = require('lodash-node/compat/object/merge'); +var omit = require('lodash-compat/object/omit'); +var merge = require('lodash-compat/object/merge'); // safely require the peer-dependencies var React = util.safeRequire('react'); @@ -118,6 +118,7 @@ exports.create = function create(createOptions) { try { if (this.useRouter) { + return match({ routes:createOptions.routes, location:thing}, function(error, redirectLocation, renderProps) { if (redirectLocation) { @@ -132,6 +133,14 @@ exports.create = function create(createOptions) { //res.send(500, error.message); } else if (renderProps) { + + // define a createElement strategy for react-router that transfers data props to all route "components" + renderProps.createElement = function(Component, routerProps) { + // for any component created by react-router, merge model data with the routerProps + // NOTE: This may be imposing too large of an opinion? + return React.createElement(Component, merge({}, data, routerProps)); + }; + return done(null, renderAndDecorate(React.createElement(RoutingContext, renderProps), data, html)); //res.send(404, 'Not found'); diff --git a/package.json b/package.json index 5fddd10..eaad918 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "debug": "^2.1.3", "glob": "^5.0.3", "history": "^1.12.2", - "lodash-node": "^3.3.1", + "lodash": "^3.10.1", + "lodash-compat": "^3.10.1", "parent-require": "^1.0.0", "react-dom": "^0.14.0" }, From 7f2794b9f80ac8783e34561ac80ff5906ea9ed4d Mon Sep 17 00:00:00 2001 From: Raymond Ho Date: Sun, 18 Oct 2015 10:14:28 +0800 Subject: [PATCH 10/28] 404 Capability Added Test Cases and Modified Edge Example --- examples/edge/index.js | 11 ++---- examples/edge/public/views/account.jsx | 8 +++-- lib/server.js | 7 ++++ lib/views/Page404.js | 30 ++++++++++++++++ test/fixtures/assertions.json | 4 ++- test/fixtures/views/page404.js | 31 +++++++++++++++++ test/server.js | 47 ++++++++++++++++++++++++++ 7 files changed, 125 insertions(+), 13 deletions(-) create mode 100644 lib/views/Page404.js create mode 100644 test/fixtures/views/page404.js diff --git a/examples/edge/index.js b/examples/edge/index.js index a3d6034..d28ccf0 100644 --- a/examples/edge/index.js +++ b/examples/edge/index.js @@ -27,7 +27,8 @@ var app = express(); // create the view engine with `react-engine` var engine = renderer.server.create({ routes: require(path.join(__dirname, '/public/routes.jsx')), - routesFilePath: path.join(__dirname, '/public/routes.jsx') + routesFilePath: path.join(__dirname, '/public/routes.jsx'), + page404: require(path.join(__dirname, '/public/views/404.jsx')) }); // set the engine @@ -69,14 +70,6 @@ app.use('/', function(req, res, next) { next(); }); -// 404 template -app.use(function(req, res) { - res.render('404', { - title: 'React Engine Express Sample App', - url: req.url - }); -}); - var server = app.listen(3000, function() { var host = server.address().address; diff --git a/examples/edge/public/views/account.jsx b/examples/edge/public/views/account.jsx index 333448f..d4b0744 100644 --- a/examples/edge/public/views/account.jsx +++ b/examples/edge/public/views/account.jsx @@ -25,9 +25,11 @@ export default React.createClass({ ); } diff --git a/lib/server.js b/lib/server.js index d84c8d7..2e334c9 100644 --- a/lib/server.js +++ b/lib/server.js @@ -41,6 +41,8 @@ var TEMPLATE = ['", "PROFILE_OUTPUT_WITH_REACT_ATTRS": "Hello, world!

Joshua

", - "ACCOUNT_OUTPUT": "Hello, world!

Joshua

" + "ACCOUNT_OUTPUT": "Hello, world!

Joshua

", + "DEFAULT404_OUTPUT": "

404 Page Not Found

", + "CUSTOM404_OUTPUT": "

Custom 404 Page Not Found - SorryJoshua

" } diff --git a/test/fixtures/views/page404.js b/test/fixtures/views/page404.js new file mode 100644 index 0000000..0edd9c2 --- /dev/null +++ b/test/fixtures/views/page404.js @@ -0,0 +1,31 @@ +'use strict'; + +var React = require('react'); + +module.exports = React.createClass({ + + render: function render() { + + return React.createElement( + 'html', + null, + React.createElement( + 'head', + null, + React.createElement('meta', { + charSet: 'utf-8' + }) + ), + React.createElement( + 'body', + null, + React.createElement( + 'h1', + null, + 'Custom 404 Page Not Found - Sorry', + this.props.name + ) + ) + ); + } +}); diff --git a/test/server.js b/test/server.js index 090a52f..68e02e1 100644 --- a/test/server.js +++ b/test/server.js @@ -297,3 +297,50 @@ test('all keys in express render `renderOptionsKeysToFilter` should be used to f }; setup(options); }); + +test('route not found and a default 404 is rendered', function(t) { + + var options = { + engine: renderer.create({ + routes: require(path.join(__dirname + '/fixtures/reactRoutes')) + }), + expressRoutes: function(req, res) { + res.render(req.url, DATA_MODEL); + }, + + onSetup: function(done) { + inject('/nonexistentpath', function(err, data) { + t.error(err); + var $ = cheerio.load(data); + $('*').removeAttr('data-reactid').removeAttr('data-react-checksum'); + t.strictEqual($.html(), assertions.DEFAULT404_OUTPUT); + done(t); + }); + } + }; + setup(options); +}); + +test('route not found and a custom 404 is rendered with data', function(t) { + + var options = { + engine: renderer.create({ + routes: require(path.join(__dirname + '/fixtures/reactRoutes')), + page404: require(path.join(__dirname + '/fixtures/views/page404')) + }), + expressRoutes: function(req, res) { + res.render(req.url, DATA_MODEL); + }, + + onSetup: function(done) { + inject('/nonexistentpath', function(err, data) { + t.error(err); + var $ = cheerio.load(data); + $('*').removeAttr('data-reactid').removeAttr('data-react-checksum'); + t.strictEqual($.html(), assertions.CUSTOM404_OUTPUT); + done(t); + }); + } + }; + setup(options); +}); From d490465c6d398292f5820ca17ad70397b4fa8967 Mon Sep 17 00:00:00 2001 From: Raymond Ho Date: Sun, 18 Oct 2015 11:37:16 +0800 Subject: [PATCH 11/28] README updated --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bc70ba3..cb23498 100644 --- a/README.md +++ b/README.md @@ -75,11 +75,12 @@ The options object can contain properties from [react router's create configurat Additionally, it can contain the following **optional** properties, -- `routesFilePath`: - path for the file that contains the react router routes. +- `routesFilePath`: \ - path for the file that contains the react router routes. react-engine uses this behind the scenes to reload the routes file in 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. -- `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) -- `performanceCollector`: - to collects [perf stats](#performance-profiling) +- `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) +- `performanceCollector`: \ - to collects [perf stats](#performance-profiling) +- `page404`: \ - This option allows you to define a custom 404 page when a URL is not matched in your routes. If left undeclared, a default 404 page, interally provided by this library, is rendered. ###### Rendering views on server side ```js From 774f6d72802e47eb9b75f066fe38954f06ba05f6 Mon Sep 17 00:00:00 2001 From: Raymond Ho Date: Mon, 19 Oct 2015 23:23:42 +0800 Subject: [PATCH 12/28] 404 Capability Follow Up * documentation udpated * adding URL for 404 reporting --- README.md | 2 +- lib/server.js | 4 +++- lib/views/Page404.js | 4 +++- test/fixtures/assertions.json | 4 ++-- test/fixtures/views/page404.js | 4 +++- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index cb23498..8595bf1 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ Additionally, it can contain the following **optional** properties, 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. - `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) - `performanceCollector`: \ - to collects [perf stats](#performance-profiling) -- `page404`: \ - This option allows you to define a custom 404 page when a URL is not matched in your routes. If left undeclared, a default 404 page, interally provided by this library, is rendered. +- `page404`: \ - This option allows you to define a custom 404 page when a URL is not matched in your routes. If left undeclared, a default 404 page, internally provided by this library, is rendered. ###### Rendering views on server side ```js diff --git a/lib/server.js b/lib/server.js index 2e334c9..08d6071 100644 --- a/lib/server.js +++ b/lib/server.js @@ -152,7 +152,9 @@ exports.create = function create(createOptions) { var component = React.createFactory(createOptions.page404 || Default404); - return done(null, renderAndDecorate(component(data), data, html)); + var mergedData = merge({}, data, {url:thing}); + + return done(null, renderAndDecorate(component(mergedData), mergedData, html)); } }); } else { diff --git a/lib/views/Page404.js b/lib/views/Page404.js index eb5dabf..73c1460 100644 --- a/lib/views/Page404.js +++ b/lib/views/Page404.js @@ -22,7 +22,9 @@ module.exports = React.createClass({ React.createElement( 'h1', null, - '404 Page Not Found' + 'URL: ', + this.props.url, + ' - 404 Page Not Found' ) ) ); diff --git a/test/fixtures/assertions.json b/test/fixtures/assertions.json index cdbd961..6601e3d 100644 --- a/test/fixtures/assertions.json +++ b/test/fixtures/assertions.json @@ -2,6 +2,6 @@ "PROFILE_OUTPUT": "Hello, world!

Joshua

", "PROFILE_OUTPUT_WITH_REACT_ATTRS": "Hello, world!

Joshua

", "ACCOUNT_OUTPUT": "Hello, world!

Joshua

", - "DEFAULT404_OUTPUT": "

404 Page Not Found

", - "CUSTOM404_OUTPUT": "

Custom 404 Page Not Found - SorryJoshua

" + "DEFAULT404_OUTPUT": "

URL: /nonexistentpath - 404 Page Not Found

", + "CUSTOM404_OUTPUT": "

URL: /nonexistentpath - Custom 404 Page Not Found - SorryJoshua

" } diff --git a/test/fixtures/views/page404.js b/test/fixtures/views/page404.js index 0edd9c2..ee5ea35 100644 --- a/test/fixtures/views/page404.js +++ b/test/fixtures/views/page404.js @@ -22,7 +22,9 @@ module.exports = React.createClass({ React.createElement( 'h1', null, - 'Custom 404 Page Not Found - Sorry', + 'URL: ', + this.props.url, + ' - Custom 404 Page Not Found - Sorry', this.props.name ) ) From fa0e6d58e79525280f375c31cf51ec981d80894e Mon Sep 17 00:00:00 2001 From: "Selvanathan, Sam" Date: Mon, 26 Oct 2015 16:46:50 -0700 Subject: [PATCH 13/28] fix lint errors --- lib/client.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/client.js b/lib/client.js index 0f8d33f..e70b053 100644 --- a/lib/client.js +++ b/lib/client.js @@ -61,6 +61,7 @@ exports.boot = function boot(options, callback) { createElement: function(Component, routerProps) { return React.createElement(Component, merge({}, props, routerProps)); }, + routes: options.routes, history: routerHistory.createHistory() }); From 3e8c36515724f0f1ae4e437da7701e65647c8716 Mon Sep 17 00:00:00 2001 From: "Selvanathan, Sam" Date: Mon, 26 Oct 2015 17:52:22 -0700 Subject: [PATCH 14/28] added the missing pkg so that builds can pass --- package.json | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 00086af..9b800b3 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "history": "^1.12.2", "lodash": "^3.10.1", "lodash-compat": "^3.10.1", + "lodash-node": "^3.10.1", "parent-require": "^1.0.0", "react-dom": "^0.14.0" }, @@ -37,14 +38,11 @@ "jscs": "^1.11.3", "jsdom": "3.0.0", "jshint": "^2.6.3", - "react": "^0.13.3", - "react-router": "^0.13.4", + "react": "^0.14.0", + "react-router": "^1.0.0-rc3", "rewire": "^2.3.1", "sinon": "^1.14.1", "tape": "^3.5.0", - "react": "^0.14.0", - "react-router": "^1.0.0-rc3", - "express": "^4.12", "time-grunt": "^1.2.1" }, "keywords": [ From b11c2210ee282070248e0e1382dd7f6111ac0f00 Mon Sep 17 00:00:00 2001 From: "Selvanathan, Sam" Date: Mon, 26 Oct 2015 17:58:57 -0700 Subject: [PATCH 15/28] bump the pkg version to alpha.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9b800b3..c43b480 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-engine", - "version": "3.0.0-rc1", + "version": "3.0.0-alpha.1", "description": "a composite render engine for express apps to render both plain react views and react-router views", "main": "index.js", "scripts": { From 786a9f7bb02facb9bc70642ab00807ffe41e3255 Mon Sep 17 00:00:00 2001 From: "Selvanathan, Sam" Date: Fri, 25 Dec 2015 12:17:25 -0800 Subject: [PATCH 16/28] remove edge example and the default views --- README.md | 2 +- examples/edge/.eslintignore | 1 - examples/edge/.eslintrc | 17 ------ examples/edge/README.md | 11 ---- examples/edge/index.js | 79 ------------------------- examples/edge/package.json | 20 ------- examples/edge/public/index.js | 40 ------------- examples/edge/public/routes.jsx | 29 --------- examples/edge/public/views/404.jsx | 30 ---------- examples/edge/public/views/account.jsx | 36 ----------- examples/edge/public/views/layout.jsx | 37 ------------ examples/edge/public/views/messages.jsx | 38 ------------ lib/views/Page404.js | 32 ---------- 13 files changed, 1 insertion(+), 371 deletions(-) delete mode 100644 examples/edge/.eslintignore delete mode 100644 examples/edge/.eslintrc delete mode 100644 examples/edge/README.md delete mode 100644 examples/edge/index.js delete mode 100644 examples/edge/package.json delete mode 100644 examples/edge/public/index.js delete mode 100644 examples/edge/public/routes.jsx delete mode 100644 examples/edge/public/views/404.jsx delete mode 100644 examples/edge/public/views/account.jsx delete mode 100644 examples/edge/public/views/layout.jsx delete mode 100644 examples/edge/public/views/messages.jsx delete mode 100644 lib/views/Page404.js diff --git a/README.md b/README.md index 2ee9411..514403d 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ ### Install ```sh # In your express app, react-engine needs to be installed along side react and optionally react-router -npm install react-engine@2 react@0.13 react-router@0.13 --save +npm install react-engine react react-router history --save ``` ### Usage On Server Side diff --git a/examples/edge/.eslintignore b/examples/edge/.eslintignore deleted file mode 100644 index 7f3dd62..0000000 --- a/examples/edge/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -public/bundle.js diff --git a/examples/edge/.eslintrc b/examples/edge/.eslintrc deleted file mode 100644 index 36b1ed7..0000000 --- a/examples/edge/.eslintrc +++ /dev/null @@ -1,17 +0,0 @@ -{ - "rules": { - "no-undef": 2, - "no-unused-vars": [2, {"vars": "all", "args": "after-used", "varsIgnorePattern": "React"}] - }, - "env": { - "node": true, - "es6": true - }, - "ecmaFeatures": { - "modules": true, - "jsx": true - }, - "globals": { - "React": true - } -} diff --git a/examples/edge/README.md b/examples/edge/README.md deleted file mode 100644 index 0960deb..0000000 --- a/examples/edge/README.md +++ /dev/null @@ -1,11 +0,0 @@ -#### a complex express app to demonstrate the usage of react-engine - -##### Uses react views and react-router views - -###### to run -```shell -cd `into_this_dir` -npm install -npm start -open http://localhost:3000 -``` diff --git a/examples/edge/index.js b/examples/edge/index.js deleted file mode 100644 index d28ccf0..0000000 --- a/examples/edge/index.js +++ /dev/null @@ -1,79 +0,0 @@ -/*-------------------------------------------------------------------------------------------------------------------*\ -| Copyright (C) 2015 PayPal | -| | -| Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | -| with the License. | -| | -| You may obtain a copy of the License at | -| | -| http://www.apache.org/licenses/LICENSE-2.0 | -| | -| Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | -| on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | -| the specific language governing permissions and limitations under the License. | -\*-------------------------------------------------------------------------------------------------------------------*/ - -'use strict'; - -// make `.jsx` file requirable by node -require('babel/register'); - -var path = require('path'); -var express = require('express'); -var renderer = require('react-engine'); - -var app = express(); - -// create the view engine with `react-engine` -var engine = renderer.server.create({ - routes: require(path.join(__dirname, '/public/routes.jsx')), - routesFilePath: path.join(__dirname, '/public/routes.jsx'), - page404: require(path.join(__dirname, '/public/views/404.jsx')) -}); - -// set the engine -app.engine('.jsx', engine); - -// set the view directory -app.set('views', path.join(__dirname, '/public/views')); - -// set jsx as the view engine -app.set('view engine', 'jsx'); - -// finally, set the custom view -app.set('view', renderer.expressView); - -//expose public folder as static assets -app.use(express.static(path.join(__dirname, '/public'))); - -// match everything and work from there -app.use('/', function(req, res, next) { - var model = { - title: 'React Engine Express Sample App', - name: 'Jordan' - }; - - if (req.originalUrl === '/messages') { - model.name = 'Messages'; - model.messages = [ - {id: 1, text: 'Lorem'}, - {id: 2, text: 'Ipsum'}, - {id: 3, text: 'Dolor'} - ]; - } - - // hack to ignore favicon.ico - if (req.originalUrl !== '/favicon.ico') { - return res.render(req.originalUrl, model); - } - - next(); -}); - -var server = app.listen(3000, function() { - - var host = server.address().address; - var port = server.address().port; - - console.log('Example app listening at http://%s:%s', host, port); -}); diff --git a/examples/edge/package.json b/examples/edge/package.json deleted file mode 100644 index 32634b3..0000000 --- a/examples/edge/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "react-engine-example-edge", - "main": "index.js", - "scripts": { - "start": "npm run browserify && node index.js", - "browserify": "browserify -t babelify -t require-globify public/index.js -o public/bundle.js" - }, - "dependencies": { - "babel": "^5.8.23", - "browserify": "^9.0.4", - "express": "^4.12.3", - "react": "^0.14", - "react-engine": "../../", - "react-router": "1.0.0-rc3", - "require-globify": "^1.1.0" - }, - "devDependencies": { - "babelify": "^6.3.0" - } -} diff --git a/examples/edge/public/index.js b/examples/edge/public/index.js deleted file mode 100644 index 9afcff9..0000000 --- a/examples/edge/public/index.js +++ /dev/null @@ -1,40 +0,0 @@ -/*-------------------------------------------------------------------------------------------------------------------*\ -| Copyright (C) 2015 PayPal | -| | -| Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | -| with the License. | -| | -| You may obtain a copy of the License at | -| | -| http://www.apache.org/licenses/LICENSE-2.0 | -| | -| Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | -| on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | -| the specific language governing permissions and limitations under the License. | -\*-------------------------------------------------------------------------------------------------------------------*/ -/* global document */ - -'use strict'; - -var Routes = require('./routes.jsx'); -var Client = require('react-engine/lib/client'); - -// Include all view files. Browerify doesn't do -// this automatically as it can only operate on -// static require statements. -require('./views/**/*.jsx', {glob: true}); - -// boot options -var options = { - routes: Routes, - - // supply a function that can be called - // to resolve the file that was rendered. - viewResolver: function(viewName) { - return require('./views/' + viewName); - } -}; - -document.addEventListener('DOMContentLoaded', function onLoad() { - Client.boot(options); -}); diff --git a/examples/edge/public/routes.jsx b/examples/edge/public/routes.jsx deleted file mode 100644 index 3a87de6..0000000 --- a/examples/edge/public/routes.jsx +++ /dev/null @@ -1,29 +0,0 @@ -/*-------------------------------------------------------------------------------------------------------------------*\ -| Copyright (C) 2015 PayPal | -| | -| Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | -| with the License. | -| | -| You may obtain a copy of the License at | -| | -| http://www.apache.org/licenses/LICENSE-2.0 | -| | -| Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | -| on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | -| the specific language governing permissions and limitations under the License. | -\*-------------------------------------------------------------------------------------------------------------------*/ - -import React from 'react'; -import { Route, IndexRoute, Redirect } from 'react-router'; -import Layout from './views/layout.jsx'; -import Account from './views/account.jsx'; -import Messages from './views/messages.jsx'; - -var routes = module.exports = ( - - - - - - -); \ No newline at end of file diff --git a/examples/edge/public/views/404.jsx b/examples/edge/public/views/404.jsx deleted file mode 100644 index 0dfce27..0000000 --- a/examples/edge/public/views/404.jsx +++ /dev/null @@ -1,30 +0,0 @@ -/*-------------------------------------------------------------------------------------------------------------------*\ -| Copyright (C) 2015 PayPal | -| | -| Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | -| with the License. | -| | -| You may obtain a copy of the License at | -| | -| http://www.apache.org/licenses/LICENSE-2.0 | -| | -| Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | -| on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | -| the specific language governing permissions and limitations under the License. | -\*-------------------------------------------------------------------------------------------------------------------*/ - -import Layout from './layout.jsx'; -import React from 'react'; - -export default React.createClass({ - - render() { - - return ( - -

URL: {this.props.url} - Not Found(404)

-
I am a Plain vanilla react view
-
- ); - } -}); diff --git a/examples/edge/public/views/account.jsx b/examples/edge/public/views/account.jsx deleted file mode 100644 index d4b0744..0000000 --- a/examples/edge/public/views/account.jsx +++ /dev/null @@ -1,36 +0,0 @@ -/*-------------------------------------------------------------------------------------------------------------------*\ -| Copyright (C) 2015 PayPal | -| | -| Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | -| with the License. | -| | -| You may obtain a copy of the License at | -| | -| http://www.apache.org/licenses/LICENSE-2.0 | -| | -| Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | -| on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | -| the specific language governing permissions and limitations under the License. | -\*-------------------------------------------------------------------------------------------------------------------*/ - -import React from 'react'; - -export default React.createClass({ - - displayName: 'account', - - render() { - - return ( -
-

{this.props.name}

-
I am a React Router rendered view
- -
- ); - } -}); diff --git a/examples/edge/public/views/layout.jsx b/examples/edge/public/views/layout.jsx deleted file mode 100644 index 8ccacf1..0000000 --- a/examples/edge/public/views/layout.jsx +++ /dev/null @@ -1,37 +0,0 @@ -/*-------------------------------------------------------------------------------------------------------------------*\ -| Copyright (C) 2015 PayPal | -| | -| Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | -| with the License. | -| | -| You may obtain a copy of the License at | -| | -| http://www.apache.org/licenses/LICENSE-2.0 | -| | -| Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | -| on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | -| the specific language governing permissions and limitations under the License. | -\*-------------------------------------------------------------------------------------------------------------------*/ - -import React from 'react'; - -export default React.createClass({ - - render() { - - return ( - - - - - {this.props.title} - - - - {this.props.children} - - - - ); - } -}); diff --git a/examples/edge/public/views/messages.jsx b/examples/edge/public/views/messages.jsx deleted file mode 100644 index 5c59383..0000000 --- a/examples/edge/public/views/messages.jsx +++ /dev/null @@ -1,38 +0,0 @@ -/*-------------------------------------------------------------------------------------------------------------------*\ -| Copyright (C) 2015 PayPal | -| | -| Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | -| with the License. | -| | -| You may obtain a copy of the License at | -| | -| http://www.apache.org/licenses/LICENSE-2.0 | -| | -| Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | -| on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | -| the specific language governing permissions and limitations under the License. | -\*-------------------------------------------------------------------------------------------------------------------*/ - -import React from 'react'; - -export default React.createClass({ - - displayName: 'messages', - - render() { - - return ( -
-

{this.props.name}

-
    - { - this.props.messages.map( message =>
  1. {message.text}
  2. ) - } -
- Click to go to an unhandled route - Messages - Redirects to /messages -
- ); - } -}); diff --git a/lib/views/Page404.js b/lib/views/Page404.js deleted file mode 100644 index 73c1460..0000000 --- a/lib/views/Page404.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict'; - -var React = require('react'); - -module.exports = React.createClass({ - - render: function render() { - - return React.createElement( - 'html', - null, - React.createElement( - 'head', - null, - React.createElement('meta', { - charSet: 'utf-8' - }) - ), - React.createElement( - 'body', - null, - React.createElement( - 'h1', - null, - 'URL: ', - this.props.url, - ' - 404 Page Not Found' - ) - ) - ); - } -}); From 0b1fda8559fba7728e703d3363d9034c6ff4e813 Mon Sep 17 00:00:00 2001 From: "Selvanathan, Sam" Date: Fri, 25 Dec 2015 18:11:16 -0800 Subject: [PATCH 17/28] rewrite of server.js to support react-router 1.x --- index.js | 4 +++ lib/errors.js | 42 ++++++++++++++++++++++++++++ lib/server.js | 76 ++++++++++++++++++--------------------------------- package.json | 2 +- 4 files changed, 73 insertions(+), 51 deletions(-) create mode 100644 lib/errors.js diff --git a/index.js b/index.js index 157a279..166cb28 100644 --- a/index.js +++ b/index.js @@ -18,3 +18,7 @@ exports.server = require('./lib/server'); exports.client = require('./lib/client'); exports.expressView = require('./lib/expressView'); + +exports.Router302Error = require('./lib/errors').Router302Error; +exports.Router404Error = require('./lib/errors').Router404Error; +exports.Router500Error = require('./lib/errors').Router500Error; diff --git a/lib/errors.js b/lib/errors.js new file mode 100644 index 0000000..9ebe557 --- /dev/null +++ b/lib/errors.js @@ -0,0 +1,42 @@ +/*-------------------------------------------------------------------------------------------------------------------*\ +| Copyright (C) 2015 PayPal | +| | +| Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | +| with the License. | +| | +| You may obtain a copy of the License at | +| | +| http://www.apache.org/licenses/LICENSE-2.0 | +| | +| Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | +| on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | +| the specific language governing permissions and limitations under the License. | +\*-------------------------------------------------------------------------------------------------------------------*/ + +'use strict'; + +var util = require('util'); + +// Creating custom Error classes in Node.js +// https://gist.github.com/justmoon/15511f92e5216fa2624b + +var Router302Error = exports.Router302Error = function Router302Error(redirectLocation) { + Error.captureStackTrace(this, this.constructor); + this.name = this.constructor.name; + this.redirectLocation = redirectLocation; +}; + +var Router404Error = exports.Router404Error = function Router404Error() { + Error.captureStackTrace(this, this.constructor); + this.name = this.constructor.name; +}; + +var Router500Error = exports.Router500Error = function Router500Error(message) { + Error.captureStackTrace(this, this.constructor); + this.name = this.constructor.name; + this.message = message; +}; + +util.inherits(Router302Error, Error); +util.inherits(Router404Error, Error); +util.inherits(Router500Error, Error); diff --git a/lib/server.js b/lib/server.js index f64c3b9..3fb2a71 100644 --- a/lib/server.js +++ b/lib/server.js @@ -14,12 +14,14 @@ \*-------------------------------------------------------------------------------------------------------------------*/ 'use strict'; -var debug = require('debug')(require('../package').name); + var path = require('path'); var util = require('./util'); var assert = require('assert'); +var Errors = require('./errors'); var Config = require('./config'); var ReactDOMServer = require('react-dom/server'); +var debug = require('debug')(require('../package').name); var format = require('util').format; var Performance = require('./performance'); @@ -40,9 +42,6 @@ var TEMPLATE = ['' ].join(''); -var TEMPLATE_REDIRECT = ''; - -var Default404 = require('./views/Page404'); exports.create = function create(createOptions) { @@ -105,7 +104,15 @@ exports.create = function create(createOptions) { // state (script) injection var script = format(TEMPLATE, Config.client.markupId, JSON.stringify(data)); - html = html.replace('', script + ''); + if (createOptions.docType === '') { + // if the `docType` is empty, the user did not want to add a docType to the rendered component, + // which means they might not be rendering a full page with `html` and `body` tags + // so attach the script tag to just the end of the generated html string + html += script; + } + else { + html = html.replace('', script + ''); + } return html; } @@ -135,43 +142,31 @@ exports.create = function create(createOptions) { try { if (this.useRouter) { - - return match({ routes:createOptions.routes, location:thing}, - function(error, redirectLocation, renderProps) { - if (redirectLocation) { - debug('server.js match 302 %s', redirectLocation); - - // perform a client-side redirect, see https://github.com/rackt/history/blob/master/docs/Location.md - return done(null, format(TEMPLATE_REDIRECT, redirectLocation.pathname + redirectLocation.search)); - - //res.redirect(302, redirectLocation.pathname + redirectLocation.search); - } else if (error) { - debug('server.js match 500 %s', error); - - //res.send(500, error.message); + return match({ routes:createOptions.routes, location:thing}, function reactRouterMatchHandler(error, redirectLocation, renderProps) { + if (error) { + debug('server.js match 500 %s', error.message); + var err = new Errors.Router500Error(error.message); + return done(err); + } else if (redirectLocation) { + debug('server.js match 302 %s', redirectLocation.pathname + redirectLocation.search); + var err = new Errors.Router302Error(redirectLocation.pathname + redirectLocation.search); + return done(err); } else if (renderProps) { - // define a createElement strategy for react-router that transfers data props to all route "components" renderProps.createElement = function(Component, routerProps) { // for any component created by react-router, merge model data with the routerProps // NOTE: This may be imposing too large of an opinion? return React.createElement(Component, merge({}, data, routerProps)); }; - return done(null, renderAndDecorate(React.createElement(RoutingContext, renderProps), data, html)); - - //res.send(404, 'Not found'); } else { - debug('server.js match 404 %s', renderProps); - - var component = React.createFactory(createOptions.page404 || Default404); - - var mergedData = merge({}, data, {url:thing}); - - return done(null, renderAndDecorate(component(mergedData), mergedData, html)); + debug('server.js match 404'); + var err = new Errors.Router404Error(); + return done(err); } }); - } else { + } + else { // path utility to make path string compatible in different OS // ------------------------------------------------------------ // use `path.normalize()` to normalzie absolute view file path and absolute base directory path @@ -188,25 +183,6 @@ exports.create = function create(createOptions) { var component = React.createFactory(view); return done(null, renderAndDecorate(component(data), data, html)); } - - // to do figure out where this should go? - // // render the componentInstance - // html += React.renderToString(componentInstance); - // - // // state (script) injection - // var script = format(TEMPLATE, Config.client.markupId, JSON.stringify(data)); - // - // if (createOptions.docType === '') { - // // if the `docType` is empty, the user did not want to add a docType to the rendered component, - // // which means they might not be rendering a full page with `html` and `body` tags - // // so attach the script tag to just the end of the generated html string - // html += script; - // } - // else { - // html = html.replace('', script + ''); - // } - // - // return done(null, html); } catch (err) { diff --git a/package.json b/package.json index 3688875..fffe696 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-engine", - "version": "3.0.0", + "version": "3.0.0-alpha.2", "description": "a composite render engine for express apps to render both plain react views and react-router views", "main": "index.js", "scripts": { From 12f763e642414387ce3cef8b7075827b8a7e4023 Mon Sep 17 00:00:00 2001 From: "Selvanathan, Sam" Date: Fri, 25 Dec 2015 18:29:32 -0800 Subject: [PATCH 18/28] fix the example app to use react-engine 3.x --- example/package.json | 7 ++++--- example/public/routes.jsx | 10 ++++++---- example/public/views/detail.jsx | 5 +---- example/public/views/layout.jsx | 4 ++-- example/public/views/list.jsx | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/example/package.json b/example/package.json index bdc3bea..553de5b 100644 --- a/example/package.json +++ b/example/package.json @@ -12,10 +12,11 @@ "babel-preset-react": "^6.3.13", "babel-register": "^6.3.13", "express": "^4.13.3", + "history": "^1.17.0", "json-loader": "^0.5.4", - "react": "^0.13.3", - "react-engine": "^2.6.0", - "react-router": "^0.13.5", + "react": "^0.14.3", + "react-engine": "3.0.0-alpha.2", + "react-router": "^1.0.3", "webpack": "^1.12.9" } } diff --git a/example/public/routes.jsx b/example/public/routes.jsx index 4c97770..fbc2da0 100644 --- a/example/public/routes.jsx +++ b/example/public/routes.jsx @@ -24,8 +24,10 @@ var ListPage = require('./views/list.jsx'); var DetailPage = require('./views/detail.jsx'); var routes = module.exports = ( - - - - + + + + + + ); diff --git a/example/public/views/detail.jsx b/example/public/views/detail.jsx index cf701fd..f196b62 100644 --- a/example/public/views/detail.jsx +++ b/example/public/views/detail.jsx @@ -16,14 +16,11 @@ 'use strict'; var React = require('react'); -var Router = require('react-router'); module.exports = React.createClass({ - mixins: [Router.State], - render: function render() { - var movieId = this.getParams().id; + var movieId = this.props.params.id; var movie = this.props.movies.filter(function(_movie) { return _movie.id === movieId; })[0]; diff --git a/example/public/views/layout.jsx b/example/public/views/layout.jsx index 66df3a5..6ce0eba 100644 --- a/example/public/views/layout.jsx +++ b/example/public/views/layout.jsx @@ -31,8 +31,8 @@ module.exports = React.createClass({
- {/* Component that renders the active child route handler of a parent route handler component. */} - + {/* Router now automatically populates this.props.children of your components based on the active route. https://github.com/rackt/react-router/blob/latest/CHANGES.md#routehandler */} + {this.props.children}
diff --git a/example/public/views/list.jsx b/example/public/views/list.jsx index 242dac7..adb9faf 100644 --- a/example/public/views/list.jsx +++ b/example/public/views/list.jsx @@ -30,7 +30,7 @@ module.exports = React.createClass({ {this.props.movies.map(function(movie) { return (
  • - + {movie.title}
  • From 09c743e9b60693d3669dc041f4e066fefeb37782 Mon Sep 17 00:00:00 2001 From: "Selvanathan, Sam" Date: Sun, 27 Dec 2015 17:52:26 -0800 Subject: [PATCH 19/28] rewrite parts of the example app using ES6 --- example/index.js | 44 ++---------------------------- example/package.json | 2 ++ example/public/favicon.ico | Bin 0 -> 5430 bytes example/public/routes.jsx | 19 +++++++------ example/public/views/layout.jsx | 1 - example/public/views/list.jsx | 2 +- example/server.js | 46 ++++++++++++++++++++++++++++++++ example/webpack.config.js | 5 +++- package.json | 2 +- 9 files changed, 65 insertions(+), 56 deletions(-) create mode 100644 example/public/favicon.ico create mode 100644 example/server.js diff --git a/example/index.js b/example/index.js index 3120305..3c6cf94 100644 --- a/example/index.js +++ b/example/index.js @@ -16,47 +16,7 @@ 'use strict'; require('babel-register')({ - presets: ['react'] + presets: ['es2015', 'react'] }); -var PORT = 3000; -var path = require('path'); -var movies = require('./movies.json'); -var express = require('express'); -var renderer = require('react-engine'); - -var app = express(); - -// create the view engine with `react-engine` -var reactRoutesFilePath = path.join(__dirname + '/public/routes.jsx'); - -var engine = renderer.server.create({ - routes: require(reactRoutesFilePath), - routesFilePath: reactRoutesFilePath -}); - -// set the engine -app.engine('.jsx', engine); - -// set the view directory -app.set('views', path.join(__dirname, '/public/views')); - -// set jsx as the view engine -app.set('view engine', 'jsx'); - -// finally, set the custom view -app.set('view', renderer.expressView); - -// expose public folder as static assets -app.use(express.static(path.join(__dirname, '/public'))); - -// add the our app routes -app.get('*', function(req, res) { - res.render(req.url, { - movies: movies - }); -}); - -var server = app.listen(PORT, function() { - console.log('Example app listening at http://localhost:%s', PORT); -}); +require('./server'); diff --git a/example/package.json b/example/package.json index 553de5b..a7e9e82 100644 --- a/example/package.json +++ b/example/package.json @@ -9,6 +9,7 @@ "dependencies": { "babel-core": "^6.3.17", "babel-loader": "^6.2.0", + "babel-preset-es2015": "^6.3.13", "babel-preset-react": "^6.3.13", "babel-register": "^6.3.13", "express": "^4.13.3", @@ -17,6 +18,7 @@ "react": "^0.14.3", "react-engine": "3.0.0-alpha.2", "react-router": "^1.0.3", + "serve-favicon": "^2.3.0", "webpack": "^1.12.9" } } diff --git a/example/public/favicon.ico b/example/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..78713f95a2a32253a8990262b7f3c687dacc1ac0 GIT binary patch literal 5430 zcmeHLdr(wW7(e&2iNhbsG&WHYkeLrqLoJ&e1;)&A_=AeZ$w+HN5JF?dMnepb?eCHCv5uUiX5ExxZu00_$2qE?`;_qMyAq{Xp3+CVv;swBY!v*uhFUA;oiYLPc zbZF2~f4|?Inqau6)b4TluR6oy{EZ`osnU(;QW93FT%Gf{_|GfXD7%4EB{xEoN!ah{ zPU^wIdy^(whmLmj^xCTOC3U}6E=F7gl*j|*-9MPuug)N>EpzffXNTHpqWO<){-9Ad zo3I3TBVt_%%UZ=#jl#^2e(25u8`Pcim}vgvyVm~_1N?Ds7$J2dENul(+cFSl9eT1M z21*VFOf>(=RM{RGtXT&9h?u#ACBMVd=I?l_K4j+Cfy7t3nevE-@nfAA<3brIx6-9@iE!rE}c;V#ATiUMYA4R9j^|cRa==CoT}}<;V>ROy1Q;VM$er*up8n};E$R`*vX9& zT?gi%sRmdY+rdXYIfPwRPHR=GnrB$6gKltelFVybYrKd*8tjp^N}}t6_uhc@c8Y*H zwL?7+$0q951<(AUaBmzKxNB>z4#|GYPbG@@(GF5Tny(F2thJ#>KDD9B&z{l!xPhmo zVLZK*4EYK5akzglN2-2K3$&L1;Py@Xk{6imkGUv?JP3S=W9u9^<}X43%zZhXV~NXc zXhjrHJHZ};Ejl1xYYr_bx0b&!<3P|J5x)=zp|5c)*o9+X3`39;==37THoU^I+yI_- zK&?U@+Q1Lj(x2_4LprN+qWa9~W#aWCKgQ!%uLHmHgZzR%20g$wg&U1_z&Jo1aKE(1 z2JLMs=lC}#-HQ{&0P=^;+&X@!eQoCC-o z4t3yR{HVja7dWO^%hQ{X11*Q_Z_}as&soEd&!(*LIxEfrv_<3`@__FJ+l$A*mpQg` z5yyISA^r<@MOn*@&!>MAoGOk1%z+rF#UZa6eSmhb2#Ck6vpIGqds`f(L)>`ykIDCj z4$lG90euiQ(=xV0t=t8B=5Bblx#_a&c(_rfx~yb{c>O{Sq8~689)_g(gf-S)cx$}; zs+PZ|$1jxVaLoeuSVxT8#HCZUy}h@mjF-Q^|BhYKyN()h4vd8#_5F4sVFd>gwv3ls zkc;8j8a|uPRCY zI=7-gu^IYTtM}YT(ebJ5lW~R|!A~$Hy3`SjnJ`2mm=T^#Pn-d;hY}J2@HD}h5%}Xl zc(OuMU(*N;`llD#_<^LO~m^TWK0uS3sj=ub|E>>sPV+;8zEtbqxOc-kuGDav2T zUsg6&`P?mQa#0@6RJ19@DBqec(Ot>&IDfUsw?I>$7C73O`qX~uPOPtZCai&86VF2k z;arAmYANzAwzTKgo;doEP|qXh6B@PHhQ=)A>46V^ny5I-<9KUd`C}RWb#PzxTnOwZP$-Bz+Og>7kU@$9~%-0?Wv1M8c81KQhZ#S z3^s&b1bbn8hX9n%32{>QX;sdaJ)#X`fL8Hn3KmS4TfkHC`P)C^HOh})t S2{teygx^yLxd;EF4B{VbL36kO literal 0 HcmV?d00001 diff --git a/example/public/routes.jsx b/example/public/routes.jsx index fbc2da0..2ab21b3 100644 --- a/example/public/routes.jsx +++ b/example/public/routes.jsx @@ -15,19 +15,18 @@ 'use strict'; -// import react and react-router -var React = require('react'); -var Router = require('react-router'); +import React from 'react'; +import { Router, Route, IndexRoute } from 'react-router'; -var Layout = require('./views/layout.jsx'); -var ListPage = require('./views/list.jsx'); -var DetailPage = require('./views/detail.jsx'); +import Layout from './views/layout.jsx'; +import ListPage from './views/list.jsx'; +import DetailPage from './views/detail.jsx'; var routes = module.exports = ( - - - - + + + + ); diff --git a/example/public/views/layout.jsx b/example/public/views/layout.jsx index 6ce0eba..4e44c16 100644 --- a/example/public/views/layout.jsx +++ b/example/public/views/layout.jsx @@ -16,7 +16,6 @@ 'use strict'; var React = require('react'); -var Router = require('react-router'); module.exports = React.createClass({ diff --git a/example/public/views/list.jsx b/example/public/views/list.jsx index adb9faf..12cc804 100644 --- a/example/public/views/list.jsx +++ b/example/public/views/list.jsx @@ -29,7 +29,7 @@ module.exports = React.createClass({
      {this.props.movies.map(function(movie) { return ( -
    • +
    • {movie.title} diff --git a/example/server.js b/example/server.js new file mode 100644 index 0000000..adcf260 --- /dev/null +++ b/example/server.js @@ -0,0 +1,46 @@ +'use strict'; + +const PORT = 3000; + +import {join} from 'path'; +import express from 'express'; +import favicon from 'serve-favicon'; +import renderer from 'react-engine'; +import movies from './movies.json'; +import routes from './public/routes.jsx'; + +let app = express(); + +// create the view engine with `react-engine` +let engine = renderer.server.create({ + routes: routes, + routesFilePath: join(__dirname, '/public/routes.jsx') +}); + +// set the engine +app.engine('.jsx', engine); + +// set the view directory +app.set('views', join(__dirname, '/public/views')); + +// set jsx as the view engine +app.set('view engine', 'jsx'); + +// finally, set the custom view +app.set('view', renderer.expressView); + +// expose public folder as static assets +app.use(express.static(join(__dirname, '/public'))); + +app.use(favicon(join(__dirname, '/public/favicon.ico'))); + +// add the our app routes +app.get('*', function(req, res) { + res.render(req.url, { + movies: movies + }); +}); + +const server = app.listen(PORT, function() { + console.log('Example app listening at http://localhost:%s', PORT); +}); diff --git a/example/webpack.config.js b/example/webpack.config.js index 76c2ceb..9b12c68 100644 --- a/example/webpack.config.js +++ b/example/webpack.config.js @@ -29,7 +29,10 @@ module.exports = { { test: /\.jsx?$/, exclude: /node_modules/, - loader: 'babel?presets[]=react' + loader: 'babel-loader', + query: { + presets: ['react', 'es2015'] + } }, { test: /\.json$/, diff --git a/package.json b/package.json index fffe696..7d560db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-engine", - "version": "3.0.0-alpha.2", + "version": "3.0.0-rc.1", "description": "a composite render engine for express apps to render both plain react views and react-router views", "main": "index.js", "scripts": { From 69739fb02b6530f8313179073915d1aa1ead199a Mon Sep 17 00:00:00 2001 From: "Selvanathan, Sam" Date: Sun, 27 Dec 2015 18:01:10 -0800 Subject: [PATCH 20/28] fix warning: validateDOMNesting in the example app --- example/public/views/layout.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/public/views/layout.jsx b/example/public/views/layout.jsx index 4e44c16..d20063a 100644 --- a/example/public/views/layout.jsx +++ b/example/public/views/layout.jsx @@ -33,8 +33,8 @@ module.exports = React.createClass({ {/* Router now automatically populates this.props.children of your components based on the active route. https://github.com/rackt/react-router/blob/latest/CHANGES.md#routehandler */} {this.props.children} + - ); } From 61f7b2cfc2d4af3b6f981d90dbd01cdb0bc77916 Mon Sep 17 00:00:00 2001 From: "Selvanathan, Sam" Date: Wed, 30 Dec 2015 11:42:52 -0800 Subject: [PATCH 21/28] use error type to distinguish the react router server errors --- example/public/routes.jsx | 5 ++- example/public/views/list.jsx | 2 +- example/server.js | 44 +++++++++++++++++-- index.js | 5 +-- lib/{errors.js => reactRouterServerErrors.js} | 43 +++++++++--------- lib/server.js | 20 +++++++-- package.json | 2 +- 7 files changed, 83 insertions(+), 38 deletions(-) rename lib/{errors.js => reactRouterServerErrors.js} (67%) diff --git a/example/public/routes.jsx b/example/public/routes.jsx index 2ab21b3..5bcaebe 100644 --- a/example/public/routes.jsx +++ b/example/public/routes.jsx @@ -16,7 +16,7 @@ 'use strict'; import React from 'react'; -import { Router, Route, IndexRoute } from 'react-router'; +import { Router, Route, IndexRoute, Redirect } from 'react-router'; import Layout from './views/layout.jsx'; import ListPage from './views/list.jsx'; @@ -26,7 +26,8 @@ var routes = module.exports = ( - + + ); diff --git a/example/public/views/list.jsx b/example/public/views/list.jsx index 12cc804..eef4fc8 100644 --- a/example/public/views/list.jsx +++ b/example/public/views/list.jsx @@ -30,7 +30,7 @@ module.exports = React.createClass({ {this.props.movies.map(function(movie) { return (
    • - + {movie.title}
    • diff --git a/example/server.js b/example/server.js index adcf260..114d361 100644 --- a/example/server.js +++ b/example/server.js @@ -1,3 +1,18 @@ +/*-------------------------------------------------------------------------------------------------------------------*\ +| Copyright (C) 2015 PayPal | +| | +| Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | +| with the License. | +| | +| You may obtain a copy of the License at | +| | +| http://www.apache.org/licenses/LICENSE-2.0 | +| | +| Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | +| on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | +| the specific language governing permissions and limitations under the License. | +\*-------------------------------------------------------------------------------------------------------------------*/ + 'use strict'; const PORT = 3000; @@ -5,14 +20,14 @@ const PORT = 3000; import {join} from 'path'; import express from 'express'; import favicon from 'serve-favicon'; -import renderer from 'react-engine'; +import ReactEngine from 'react-engine'; import movies from './movies.json'; import routes from './public/routes.jsx'; let app = express(); // create the view engine with `react-engine` -let engine = renderer.server.create({ +let engine = ReactEngine.server.create({ routes: routes, routesFilePath: join(__dirname, '/public/routes.jsx') }); @@ -27,12 +42,12 @@ app.set('views', join(__dirname, '/public/views')); app.set('view engine', 'jsx'); // finally, set the custom view -app.set('view', renderer.expressView); +app.set('view', ReactEngine.expressView); // expose public folder as static assets app.use(express.static(join(__dirname, '/public'))); -app.use(favicon(join(__dirname, '/public/favicon.ico'))); +app.use(favicon(join(__dirname, '/public/favicon.ico'))); // add the our app routes app.get('*', function(req, res) { @@ -41,6 +56,27 @@ app.get('*', function(req, res) { }); }); +app.use(function(err, req, res, next) { + console.error(err); + + // http://expressjs.com/en/guide/error-handling.html + if (res.headersSent) { + return next(err); + } + + if (err._type && err._type === ReactEngine.reactRouterServerErrors.MATCH_REDIRECT) { + return res.redirect(302, err.redirectLocation); + } + else if (err._type && err._type === ReactEngine.reactRouterServerErrors.MATCH_NOT_FOUND) { + return res.status(404).send('Route Not Found!'); + } + else { + // for ReactEngine.reactRouterServerErrors.MATCH_INTERNAL_ERROR or + // any other error we just send the error message back + return res.status(500).send(err.message); + } +}); + const server = app.listen(PORT, function() { console.log('Example app listening at http://localhost:%s', PORT); }); diff --git a/index.js b/index.js index 166cb28..16cf1fd 100644 --- a/index.js +++ b/index.js @@ -18,7 +18,4 @@ exports.server = require('./lib/server'); exports.client = require('./lib/client'); exports.expressView = require('./lib/expressView'); - -exports.Router302Error = require('./lib/errors').Router302Error; -exports.Router404Error = require('./lib/errors').Router404Error; -exports.Router500Error = require('./lib/errors').Router500Error; +exports.reactRouterServerErrors = require('./lib/reactRouterServerErrors'); diff --git a/lib/errors.js b/lib/reactRouterServerErrors.js similarity index 67% rename from lib/errors.js rename to lib/reactRouterServerErrors.js index 9ebe557..50d3632 100644 --- a/lib/errors.js +++ b/lib/reactRouterServerErrors.js @@ -15,28 +15,27 @@ 'use strict'; -var util = require('util'); +var errorTypes = ['MATCH_REDIRECT', + 'MATCH_NOT_FOUND', + 'MATCH_INTERNAL_ERROR']; -// Creating custom Error classes in Node.js -// https://gist.github.com/justmoon/15511f92e5216fa2624b +var properties = {}; -var Router302Error = exports.Router302Error = function Router302Error(redirectLocation) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.redirectLocation = redirectLocation; -}; +errorTypes.map(function(errorType) { + properties[errorType] = { + configurable: false, + writable: false, + enumerable: true, + value: errorType + }; +}); -var Router404Error = exports.Router404Error = function Router404Error() { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; -}; - -var Router500Error = exports.Router500Error = function Router500Error(message) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.message = message; -}; - -util.inherits(Router302Error, Error); -util.inherits(Router404Error, Error); -util.inherits(Router500Error, Error); +/* + export the reactRouterServerErrors object + { + MATCH_REDIRECT: 'MATCH_REDIRECT', + MATCH_NOT_FOUND: 'MATCH_NOT_FOUND', + MATCH_INTERNAL_ERROR: 'MATCH_INTERNAL_ERROR' + } +*/ +module.exports = Object.create(null, properties); diff --git a/lib/server.js b/lib/server.js index 3fb2a71..cf339cc 100644 --- a/lib/server.js +++ b/lib/server.js @@ -18,10 +18,10 @@ var path = require('path'); var util = require('./util'); var assert = require('assert'); -var Errors = require('./errors'); var Config = require('./config'); var ReactDOMServer = require('react-dom/server'); var debug = require('debug')(require('../package').name); +var ReactRouterServerErrors = require('./reactRouterServerErrors'); var format = require('util').format; var Performance = require('./performance'); @@ -43,6 +43,16 @@ var TEMPLATE = ['' ].join(''); +function generateReactRouterServerError(type, existingErrorObj, additionalProperties) { + var err = existingErrorObj || new Error(); + err._type = type; + if (additionalProperties) { + merge(err, additionalProperties); + } + + return err; +} + exports.create = function create(createOptions) { // safely require the peer-dependencies @@ -145,11 +155,13 @@ exports.create = function create(createOptions) { return match({ routes:createOptions.routes, location:thing}, function reactRouterMatchHandler(error, redirectLocation, renderProps) { if (error) { debug('server.js match 500 %s', error.message); - var err = new Errors.Router500Error(error.message); + var err = generateReactRouterServerError(ReactRouterServerErrors.MATCH_INTERNAL_ERROR, error); return done(err); } else if (redirectLocation) { debug('server.js match 302 %s', redirectLocation.pathname + redirectLocation.search); - var err = new Errors.Router302Error(redirectLocation.pathname + redirectLocation.search); + var err = generateReactRouterServerError(ReactRouterServerErrors.MATCH_REDIRECT, null, { + redirectLocation: redirectLocation.pathname + redirectLocation.search + }); return done(err); } else if (renderProps) { // define a createElement strategy for react-router that transfers data props to all route "components" @@ -161,7 +173,7 @@ exports.create = function create(createOptions) { return done(null, renderAndDecorate(React.createElement(RoutingContext, renderProps), data, html)); } else { debug('server.js match 404'); - var err = new Errors.Router404Error(); + var err = generateReactRouterServerError(ReactRouterServerErrors.MATCH_NOT_FOUND); return done(err); } }); diff --git a/package.json b/package.json index 7d560db..ff0f1d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-engine", - "version": "3.0.0-rc.1", + "version": "3.0.0-rc.2", "description": "a composite render engine for express apps to render both plain react views and react-router views", "main": "index.js", "scripts": { From 23d7abbaf6eb6b75a1f508d27099af51bd8db6d0 Mon Sep 17 00:00:00 2001 From: "Selvanathan, Sam" Date: Wed, 30 Dec 2015 11:48:32 -0800 Subject: [PATCH 22/28] fix lint errors --- lib/client.js | 1 + lib/server.js | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/client.js b/lib/client.js index b23bfa0..bf32945 100644 --- a/lib/client.js +++ b/lib/client.js @@ -73,6 +73,7 @@ exports.boot = function boot(options, callback) { createElement: function(Component, routerProps) { return React.createElement(Component, merge({}, props, routerProps)); }, + routes: options.routes, history: routerHistory.createHistory() }); diff --git a/lib/server.js b/lib/server.js index cf339cc..de0317a 100644 --- a/lib/server.js +++ b/lib/server.js @@ -170,6 +170,7 @@ exports.create = function create(createOptions) { // NOTE: This may be imposing too large of an opinion? return React.createElement(Component, merge({}, data, routerProps)); }; + return done(null, renderAndDecorate(React.createElement(RoutingContext, renderProps), data, html)); } else { debug('server.js match 404'); From 46d1b1407af8903fa90eddfdd59f60e17ae94318 Mon Sep 17 00:00:00 2001 From: "Selvanathan, Sam" Date: Wed, 30 Dec 2015 11:52:41 -0800 Subject: [PATCH 23/28] modify the test cases to support the new implementation of v3.x --- test/fixtures/assertions.json | 4 +-- test/fixtures/views/page404.js | 33 ------------------------ test/server.js | 47 ---------------------------------- 3 files changed, 1 insertion(+), 83 deletions(-) delete mode 100644 test/fixtures/views/page404.js diff --git a/test/fixtures/assertions.json b/test/fixtures/assertions.json index 6601e3d..a89940b 100644 --- a/test/fixtures/assertions.json +++ b/test/fixtures/assertions.json @@ -1,7 +1,5 @@ { "PROFILE_OUTPUT": "Hello, world!

      Joshua

      ", "PROFILE_OUTPUT_WITH_REACT_ATTRS": "Hello, world!

      Joshua

      ", - "ACCOUNT_OUTPUT": "Hello, world!

      Joshua

      ", - "DEFAULT404_OUTPUT": "

      URL: /nonexistentpath - 404 Page Not Found

      ", - "CUSTOM404_OUTPUT": "

      URL: /nonexistentpath - Custom 404 Page Not Found - SorryJoshua

      " + "ACCOUNT_OUTPUT": "Hello, world!

      Joshua

      " } diff --git a/test/fixtures/views/page404.js b/test/fixtures/views/page404.js deleted file mode 100644 index ee5ea35..0000000 --- a/test/fixtures/views/page404.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -var React = require('react'); - -module.exports = React.createClass({ - - render: function render() { - - return React.createElement( - 'html', - null, - React.createElement( - 'head', - null, - React.createElement('meta', { - charSet: 'utf-8' - }) - ), - React.createElement( - 'body', - null, - React.createElement( - 'h1', - null, - 'URL: ', - this.props.url, - ' - Custom 404 Page Not Found - Sorry', - this.props.name - ) - ) - ); - } -}); diff --git a/test/server.js b/test/server.js index 68e02e1..090a52f 100644 --- a/test/server.js +++ b/test/server.js @@ -297,50 +297,3 @@ test('all keys in express render `renderOptionsKeysToFilter` should be used to f }; setup(options); }); - -test('route not found and a default 404 is rendered', function(t) { - - var options = { - engine: renderer.create({ - routes: require(path.join(__dirname + '/fixtures/reactRoutes')) - }), - expressRoutes: function(req, res) { - res.render(req.url, DATA_MODEL); - }, - - onSetup: function(done) { - inject('/nonexistentpath', function(err, data) { - t.error(err); - var $ = cheerio.load(data); - $('*').removeAttr('data-reactid').removeAttr('data-react-checksum'); - t.strictEqual($.html(), assertions.DEFAULT404_OUTPUT); - done(t); - }); - } - }; - setup(options); -}); - -test('route not found and a custom 404 is rendered with data', function(t) { - - var options = { - engine: renderer.create({ - routes: require(path.join(__dirname + '/fixtures/reactRoutes')), - page404: require(path.join(__dirname + '/fixtures/views/page404')) - }), - expressRoutes: function(req, res) { - res.render(req.url, DATA_MODEL); - }, - - onSetup: function(done) { - inject('/nonexistentpath', function(err, data) { - t.error(err); - var $ = cheerio.load(data); - $('*').removeAttr('data-reactid').removeAttr('data-react-checksum'); - t.strictEqual($.html(), assertions.CUSTOM404_OUTPUT); - done(t); - }); - } - }; - setup(options); -}); From 5efab88c321053a0607e1b99075370cc763ca12f Mon Sep 17 00:00:00 2001 From: "Selvanathan, Sam" Date: Wed, 30 Dec 2015 14:13:39 -0800 Subject: [PATCH 24/28] updated tests and read me. --- README.md | 45 +++++++++- example/Readme.md | 60 +++++++++---- example/package.json | 2 +- lib/server.js | 3 +- package.json | 5 +- test/client.js | 2 +- test/fixtures/assertions.json | 4 +- .../{reactRoutes.js => reactRoutes.jsx} | 19 ++-- .../views/{account.js => account.jsx} | 17 ++-- test/fixtures/views/{app.js => app.jsx} | 10 +-- test/fixtures/views/layout.js | 36 -------- test/fixtures/views/layout.jsx | 36 ++++++++ .../views/{profile.js => profile.jsx} | 16 ++-- test/server.js | 86 +++++++++++++++++-- 14 files changed, 234 insertions(+), 107 deletions(-) rename test/fixtures/{reactRoutes.js => reactRoutes.jsx} (84%) rename test/fixtures/views/{account.js => account.jsx} (87%) rename test/fixtures/views/{app.js => app.jsx} (91%) delete mode 100644 test/fixtures/views/layout.js create mode 100644 test/fixtures/views/layout.jsx rename test/fixtures/views/{profile.js => profile.jsx} (87%) diff --git a/README.md b/README.md index 514403d..ae58cf2 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ### Install ```sh -# In your express app, react-engine needs to be installed along side react and optionally react-router +# In your express app, react-engine needs to be installed along side react and optionally react-router (+ history, react-router's dependency) npm install react-engine react react-router history --save ``` @@ -75,14 +75,13 @@ The options object can contain properties from [react router's create configurat Additionally, it can contain the following **optional** properties, +- `docType`: \ - a string that can be used as a doctype (_Default: ``_). + (docType might not make sense if you are rendering partials/sub page components, in that case you can pass and empty string as docType) - `routesFilePath`: \ - path for the file that contains the react router routes. -- `docType`: - string that can be used as a doctype (_Default: ``_) -- `routesFilePath`: - path for the file that contains the react router routes. react-engine uses this behind the scenes to reload the routes file in 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. - `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) - `performanceCollector`: \ - to collects [perf stats](#performance-profiling) -- `page404`: \ - This option allows you to define a custom 404 page when a URL is not matched in your routes. If left undeclared, a default 404 page, internally provided by this library, is rendered. ###### Rendering views on server side ```js @@ -151,6 +150,40 @@ Note: By default, the following three keys are always filtered out from `renderO - `enrouten` - `_locals` +### Handling redirects and route not found errors on the server side +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/rackt/react-router/blob/5590516ec228765cbb176c81fb15fe1d4662e475/docs/guides/advanced/ServerRendering.md). + +To handle the above during the lifecycle of a request, add a error type check in your express middleware. The following are the three types of error that get thrown by react-engine: +| Error Type (err._type) | Description | +| ---------------------- |:-------------:| +| MATCH_REDIRECT | indicates that the url matched to a redirection (`redirectLocation` property of the err has the new redirection location) | +| MATCH_NOT_FOUND | indicates that the url did not match to any component | +| MATCH_INTERNAL_ERROR | indicates that react-router encountered an internal error | + +```javascript +// example express error middleware +app.use(function(err, req, res, next) { + console.error(err); + + // http://expressjs.com/en/guide/error-handling.html + if (res.headersSent) { + return next(err); + } + + if (err._type && err._type === ReactEngine.reactRouterServerErrors.MATCH_REDIRECT) { + return res.redirect(302, err.redirectLocation); + } + else if (err._type && err._type === ReactEngine.reactRouterServerErrors.MATCH_NOT_FOUND) { + return res.status(404).send('Route Not Found!'); + } + else { + // for ReactEngine.reactRouterServerErrors.MATCH_INTERNAL_ERROR or + // any other error we just send the error message back + return res.status(500).send(err.message); + } +}); +``` + ### Yeoman Generator There is a Yeoman generator available to create a new express or KrakenJS application which uses react-engine: [generator-react-engine](https://www.npmjs.com/package/generator-react-engine). @@ -186,6 +219,10 @@ var engine = require('react-engine').server.create({ * You can use `js` as the engine if you decide not to write your react views in `jsx`. * [Blog on react-engine](https://www.paypal-engineering.com/2015/04/27/isomorphic-react-apps-with-react-engine/) +### Migration from 2.x to 3.x +While upgrading to 3.x version of react-engine, make sure to follow the [react-router's 1.x upgrade guide](https://github.com/rackt/react-router/blob/5590516ec228765cbb176c81fb15fe1d4662e475/upgrade-guides/v1.0.0.md) to upgrade react-router related code in your app. +Then add to your express error middleware, react-engine's MATCH_REDIRECT and MATCH_NOT_FOUND checks. + ### Migration from 1.x to 2.x 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. diff --git a/example/Readme.md b/example/Readme.md index c228cd3..e594869 100644 --- a/example/Readme.md +++ b/example/Readme.md @@ -3,9 +3,9 @@ This movie catalog app illustrates the usage of react-engine to build and run an ## app composition * [express - 4.x](https://github.com/strongloop/express) on the server side -* [react-engine - 2.x](https://github.com/paypal/react-engine) as the express view render engine -* [react - 0.13.x](https://github.com/facebook/react) for building the UI -* [react-router - 0.13.x](https://github.com/rackt/react-router) for UI routing +* [react-engine - 3.x](https://github.com/paypal/react-engine) as the express view render engine +* [react - 0.14.x](https://github.com/facebook/react) for building the UI +* [react-router - 1.x](https://github.com/rackt/react-router) for UI routing * [webpack - 1.x](https://github.com/webpack/webpack) as the client side module loader * [babel - 6.x](https://github.com/babel/babel) for compiling the ES6/JSX code @@ -26,8 +26,8 @@ $ open http://localhost:3000 # (fill out the needed information like name, author, etc..) $ npm init - # install express, react, react-router & react-engine - $ npm install express react-engine@2 react@0.13 react-router@0.13 --save + # install express, react, react-router (+ history, its dependency) & react-engine + $ npm install express react-engine react react-router history --save # install the rest of the dependencies $ npm install babel-register babel-preset-react webpack --save @@ -70,10 +70,13 @@ $ open http://localhost:3000 var DetailPage = require('./views/detail.jsx'); var routes = module.exports = ( - - - - + + + + + + + ); ``` @@ -96,17 +99,19 @@ $ open http://localhost:3000 React Engine Example App +
      - {/* Component that renders the active child route handler of a parent route handler component. */} - + {/* Router now automatically populates this.props.children of your components based on the active route. https://github.com/rackt/react-router/blob/latest/CHANGES.md#routehandler */} + {this.props.children}
      + ); } - }); + }); // public/views/list.jsx file contains the catalog view elements of our app. // we iterate through the array of movies and display them on this page. @@ -119,13 +124,14 @@ $ open http://localhost:3000
        {this.props.movies.map(function(movie) { return ( -
      • - +
      • + {movie.title}
      • ); })} +
      ); @@ -135,12 +141,12 @@ $ open http://localhost:3000 // public/views/detail.jsx file contains the markup to // display the detail information of a movie module.exports = React.createClass({ - mixins: [Router.State], render: function render() { - var movieId = this.getParams().id; + var movieId = this.props.params.id; var movie = this.props.movies.filter(function(_movie) { return _movie.id === movieId; })[0]; + return (

      {movie.title}

      @@ -198,6 +204,28 @@ $ open http://localhost:3000 }); }); + // add the error handler middleware + app.use(function(err, req, res, next) { + console.error(err); + + // http://expressjs.com/en/guide/error-handling.html + if (res.headersSent) { + return next(err); + } + + if (err._type && err._type === ReactEngine.reactRouterServerErrors.MATCH_REDIRECT) { + return res.redirect(302, err.redirectLocation); + } + else if (err._type && err._type === ReactEngine.reactRouterServerErrors.MATCH_NOT_FOUND) { + return res.status(404).send('Route Not Found!'); + } + else { + // for ReactEngine.reactRouterServerErrors.MATCH_INTERNAL_ERROR or + // any other error we just send the error message back + return res.status(500).send(err.message); + } + }); + // the last step in the server side is to configure the express app to listen on port 3000 app.listen(3000, function() { console.log('Example app listening at http://localhost:%s', PORT); diff --git a/example/package.json b/example/package.json index a7e9e82..2096a75 100644 --- a/example/package.json +++ b/example/package.json @@ -16,7 +16,7 @@ "history": "^1.17.0", "json-loader": "^0.5.4", "react": "^0.14.3", - "react-engine": "3.0.0-alpha.2", + "react-engine": "3.0.0-rc.2", "react-router": "^1.0.3", "serve-favicon": "^2.3.0", "webpack": "^1.12.9" diff --git a/lib/server.js b/lib/server.js index de0317a..9eaa1aa 100644 --- a/lib/server.js +++ b/lib/server.js @@ -44,7 +44,7 @@ var TEMPLATE = ['", - "PROFILE_OUTPUT_WITH_REACT_ATTRS": "Hello, world!

      Joshua

      ", + "PROFILE_OUTPUT": "Hello, world!

      Joshua

      ", + "PROFILE_OUTPUT_WITH_REACT_ATTRS": "Hello, world!

      Joshua

      ", "ACCOUNT_OUTPUT": "Hello, world!

      Joshua

      " } diff --git a/test/fixtures/reactRoutes.js b/test/fixtures/reactRoutes.jsx similarity index 84% rename from test/fixtures/reactRoutes.js rename to test/fixtures/reactRoutes.jsx index b2162c8..b667475 100644 --- a/test/fixtures/reactRoutes.js +++ b/test/fixtures/reactRoutes.jsx @@ -18,15 +18,14 @@ var React = require('react'); var Router = require('react-router'); -var App = require('./views/app'); -var Account = require('./views/account'); +var App = require('./views/app.jsx'); +var Account = require('./views/account.jsx'); -module.exports = React.createElement( - Router, - null, - React.createElement( - Router.Route, - { path: '/', component: App }, - React.createElement(Router.Route, { path: '/account', component: Account }) - ) +var routes = module.exports = ( + + + + + + ); diff --git a/test/fixtures/views/account.js b/test/fixtures/views/account.jsx similarity index 87% rename from test/fixtures/views/account.js rename to test/fixtures/views/account.jsx index d1c6073..e0f1735 100644 --- a/test/fixtures/views/account.js +++ b/test/fixtures/views/account.jsx @@ -19,18 +19,11 @@ var React = require('react'); module.exports = React.createClass({ - displayName: 'account', - - render: function render(props) { - var name = this.props.name || 'Joshua'; - return React.createElement( - 'div', - { id: 'account' }, - React.createElement( - 'h1', - null, - name - ) + render: function() { + return ( +
      +

      {this.props.name || 'Joshua'}

      +
      ); } }); diff --git a/test/fixtures/views/app.js b/test/fixtures/views/app.jsx similarity index 91% rename from test/fixtures/views/app.js rename to test/fixtures/views/app.jsx index 3ec1ce6..45fbf77 100644 --- a/test/fixtures/views/app.js +++ b/test/fixtures/views/app.jsx @@ -17,16 +17,16 @@ var React = require('react'); var Router = require('react-router'); -var Layout = require('./layout'); +var Layout = require('./layout.jsx'); module.exports = React.createClass({ - displayName: 'app', - render: function render() { - return React.createElement(Layout, this.props, - React.cloneElement(this.props.children, this.props) + return ( + + {this.props.children} + ); } }); diff --git a/test/fixtures/views/layout.js b/test/fixtures/views/layout.js deleted file mode 100644 index 6e6635b..0000000 --- a/test/fixtures/views/layout.js +++ /dev/null @@ -1,36 +0,0 @@ -/*-------------------------------------------------------------------------------------------------------------------*\ -| Copyright (C) 2015 PayPal | -| | -| Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | -| with the License. | -| | -| You may obtain a copy of the License at | -| | -| http://www.apache.org/licenses/LICENSE-2.0 | -| | -| Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | -| on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | -| the specific language governing permissions and limitations under the License. | -\*-------------------------------------------------------------------------------------------------------------------*/ - -'use strict'; - -var React = require('react'); - -module.exports = React.createClass({ - - displayName: 'layout', - - render: function render() { - var title = this.props.title || 'Hello, world!'; - return React.createElement( - 'html', null, - React.createElement( - 'head', null, - React.createElement('meta', { charSet: 'utf-8' }), - React.createElement('title', null, title) - ), - React.createElement('body', null, this.props.children) - ); - } -}); diff --git a/test/fixtures/views/layout.jsx b/test/fixtures/views/layout.jsx new file mode 100644 index 0000000..229464f --- /dev/null +++ b/test/fixtures/views/layout.jsx @@ -0,0 +1,36 @@ +/*-------------------------------------------------------------------------------------------------------------------*\ +| Copyright (C) 2015 PayPal | +| | +| Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance | +| with the License. | +| | +| You may obtain a copy of the License at | +| | +| http://www.apache.org/licenses/LICENSE-2.0 | +| | +| Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | +| on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for | +| the specific language governing permissions and limitations under the License. | +\*-------------------------------------------------------------------------------------------------------------------*/ + +'use strict'; + +var React = require('react'); + +module.exports = React.createClass({ + + render: function() { + + return ( + + + + {this.props.title || 'Hello, world!'} + + {this.props.children} + + + + ); + } +}); diff --git a/test/fixtures/views/profile.js b/test/fixtures/views/profile.jsx similarity index 87% rename from test/fixtures/views/profile.js rename to test/fixtures/views/profile.jsx index 82554c7..b02993a 100644 --- a/test/fixtures/views/profile.js +++ b/test/fixtures/views/profile.jsx @@ -16,20 +16,18 @@ 'use strict'; var React = require('react'); -var Layout = require('./layout'); +var Layout = require('./layout.jsx'); module.exports = React.createClass({ - displayName: 'profile', - render: function render() { - return React.createElement(Layout, this.props, - React.createElement('div', { id: 'profile' }, - React.createElement( - 'h1', null, this.props.name - ) - ) + return ( + +
      +

      {this.props.name}

      +
      +
      ); } }); diff --git a/test/server.js b/test/server.js index 090a52f..0edcd69 100644 --- a/test/server.js +++ b/test/server.js @@ -15,13 +15,17 @@ 'use strict'; +require('babel-register')({ + presets: ['react'] +}); + var fs = require('fs'); var path = require('path'); var test = require('tape'); var express = require('express'); var cheerio = require('cheerio'); var renderer = require('../index').server; -var assertions = require('./fixtures/assertions'); +var assertions = require('./fixtures/assertions.json'); var DATA_MODEL = exports.DATA_MODEL = { title: 'Hello, world!', @@ -67,8 +71,8 @@ function setup(options) { }; } - app.engine('js', setupEngine); - app.set('view engine', 'js'); + app.engine('jsx', setupEngine); + app.set('view engine', 'jsx'); app.set('view cache', false); app.set('views', path.resolve(__dirname, 'fixtures/views')); @@ -91,7 +95,29 @@ function setup(options) { }); } -// start of test definitions +/* + ------------------------- + start of test definitions + ------------------------- +*/ + +test('react-engine public api', function(t) { + var index = require('../index'); + t.strictEqual(typeof index.server.create, 'function'); + t.strictEqual(typeof index.client.data, 'function'); + t.strictEqual(typeof index.client.boot, 'function'); + t.strictEqual(typeof index.expressView, 'function'); + t.strictEqual(typeof index.reactRouterServerErrors, 'object'); + t.strictEqual(index.reactRouterServerErrors.MATCH_REDIRECT, 'MATCH_REDIRECT'); + t.strictEqual(index.reactRouterServerErrors.MATCH_NOT_FOUND, 'MATCH_NOT_FOUND'); + t.strictEqual(index.reactRouterServerErrors.MATCH_INTERNAL_ERROR, 'MATCH_INTERNAL_ERROR'); + t.throws(function reactRouterServerErrorsObjectShouldNotBeModifiable() { + index.reactRouterServerErrors.MATCH_REDIRECT = '123'; + }); + + t.end(); +}); + test('construct an engine', function(t) { var engine = renderer.create(); t.ok(engine instanceof Function); @@ -151,7 +177,7 @@ test('performance collector', function(t) { t.strictEqual(typeof data, 'string'); t.strictEqual(recorder.length, 1); t.strictEqual(Object.keys(recorder[0]).length, 4); - t.strictEqual(recorder[0].name, path.resolve(__dirname, 'fixtures/views', 'profile.js')); + t.strictEqual(recorder[0].name, path.resolve(__dirname, 'fixtures/views', 'profile.jsx')); t.strictEqual(typeof recorder[0].startTime, 'number'); t.strictEqual(typeof recorder[0].endTime, 'number'); t.strictEqual(typeof recorder[0].duration, 'number'); @@ -209,7 +235,7 @@ test('router gets run when we pass urls into render function', function(t) { var options = { engine: renderer.create({ - routes: require(path.join(__dirname + '/fixtures/reactRoutes')) + routes: require(path.join(__dirname + '/fixtures/reactRoutes.jsx')) }), expressRoutes: function(req, res) { res.render(req.url, DATA_MODEL); @@ -253,7 +279,7 @@ test('all keys in express render `options` should be be sent to client', functio var options = { engine: renderer.create({ - routes: require(path.join(__dirname + '/fixtures/reactRoutes')) + routes: require(path.join(__dirname + '/fixtures/reactRoutes.jsx')) }), expressRoutes: function(req, res) { res.locals.someSensitiveData = 1234; @@ -277,7 +303,7 @@ test('all keys in express render `renderOptionsKeysToFilter` should be used to f var options = { engine: renderer.create({ - routes: require(path.join(__dirname + '/fixtures/reactRoutes')), + routes: require(path.join(__dirname + '/fixtures/reactRoutes.jsx')), renderOptionsKeysToFilter: ['someSensitiveData'] }), expressRoutes: function(req, res) { @@ -297,3 +323,47 @@ test('all keys in express render `renderOptionsKeysToFilter` should be used to f }; setup(options); }); + +test('error that renderer throws when asked to run a unknown route', function(t) { + + var options = { + engine: renderer.create({ + routes: require(path.join(__dirname + '/fixtures/reactRoutes.jsx')) + }), + expressRoutes: function(req, res) { + res.render(req.url, DATA_MODEL); + }, + + onSetup: function(done) { + inject('/some_garbage', function(err, data) { + // TODO: t.strictEqual(err._type, 'MATCH_NOT_FOUND'); + t.ok(typeof err === 'object'); + t.ok(typeof data === 'undefined'); + done(t); + }); + } + }; + setup(options); +}); + +test('error that renderer throws when asked to run a redirect route', function(t) { + + var options = { + engine: renderer.create({ + routes: require(path.join(__dirname + '/fixtures/reactRoutes.jsx')) + }), + expressRoutes: function(req, res) { + res.render(req.url, DATA_MODEL); + }, + + onSetup: function(done) { + inject('/gohome', function(err, data) { + // TODO: t.strictEqual(err._type, 'MATCH_REDIRECT'); + t.ok(typeof err === 'object'); + t.ok(typeof data === 'undefined'); + done(t); + }); + } + }; + setup(options); +}); From 9ae8eecb5e777300e6281e93fb5b4cc7c52f7257 Mon Sep 17 00:00:00 2001 From: "Selvanathan, Sam" Date: Wed, 30 Dec 2015 14:24:25 -0800 Subject: [PATCH 25/28] fix the formatting in the documentation --- README.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ae58cf2..c092c64 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ ### Install ```sh -# In your express app, react-engine needs to be installed along side react and optionally react-router (+ history, react-router's dependency) +# In your express app, react-engine needs to be installed along +# side react and optionally react-router (+ history, react-router's dependency) npm install react-engine react react-router history --save ``` @@ -154,11 +155,14 @@ Note: By default, the following three keys are always filtered out from `renderO 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/rackt/react-router/blob/5590516ec228765cbb176c81fb15fe1d4662e475/docs/guides/advanced/ServerRendering.md). To handle the above during the lifecycle of a request, add a error type check in your express middleware. The following are the three types of error that get thrown by react-engine: -| Error Type (err._type) | Description | -| ---------------------- |:-------------:| -| MATCH_REDIRECT | indicates that the url matched to a redirection (`redirectLocation` property of the err has the new redirection location) | -| MATCH_NOT_FOUND | indicates that the url did not match to any component | -| MATCH_INTERNAL_ERROR | indicates that react-router encountered an internal error | + +Error Type | Description +-------------------- | -------------------------------------------------------- +MATCH_REDIRECT** | indicates that the url matched to a redirection +MATCH_NOT_FOUND | indicates that the url did not match to any component +MATCH_INTERNAL_ERROR | indicates that react-router encountered an internal error + + _** for redirection error, `redirectLocation` property of the err has the new redirection location_ ```javascript // example express error middleware @@ -221,7 +225,7 @@ var engine = require('react-engine').server.create({ ### Migration from 2.x to 3.x While upgrading to 3.x version of react-engine, make sure to follow the [react-router's 1.x upgrade guide](https://github.com/rackt/react-router/blob/5590516ec228765cbb176c81fb15fe1d4662e475/upgrade-guides/v1.0.0.md) to upgrade react-router related code in your app. -Then add to your express error middleware, react-engine's MATCH_REDIRECT and MATCH_NOT_FOUND checks. +Then, add to your express error middleware, react-engine's MATCH_REDIRECT and MATCH_NOT_FOUND checks. ### Migration from 1.x to 2.x 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. From 7324e481c1cc370863a36faebac244b7c1e6dff3 Mon Sep 17 00:00:00 2001 From: "Selvanathan, Sam" Date: Tue, 5 Jan 2016 14:39:15 -0800 Subject: [PATCH 26/28] minor updates to doc --- README.md | 6 +++--- example/server.js | 5 ++++- package.json | 3 +-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c092c64..557e719 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ Note: By default, the following three keys are always filtered out from `renderO ### Handling redirects and route not found errors on the server side 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/rackt/react-router/blob/5590516ec228765cbb176c81fb15fe1d4662e475/docs/guides/advanced/ServerRendering.md). -To handle the above during the lifecycle of a request, add a error type check in your express middleware. The following are the three types of error that get thrown by react-engine: +To handle the above during the lifecycle of a request, add a error type check in your express error middleware. The following are the three types of error that get thrown by react-engine: Error Type | Description -------------------- | -------------------------------------------------------- @@ -162,7 +162,7 @@ MATCH_REDIRECT** | indicates that the url matched to a redirection MATCH_NOT_FOUND | indicates that the url did not match to any component MATCH_INTERNAL_ERROR | indicates that react-router encountered an internal error - _** for redirection error, `redirectLocation` property of the err has the new redirection location_ + _** for MATCH_REDIRECT error, `redirectLocation` property of the err has the new redirection location_ ```javascript // example express error middleware @@ -212,7 +212,7 @@ function collector(stats) { } var engine = require('react-engine').server.create({ - reactRoutes: './routes.jsx' + routes: './routes.jsx' performanceCollector: collector }); ``` diff --git a/example/server.js b/example/server.js index 114d361..c9f83dd 100644 --- a/example/server.js +++ b/example/server.js @@ -29,7 +29,10 @@ let app = express(); // create the view engine with `react-engine` let engine = ReactEngine.server.create({ routes: routes, - routesFilePath: join(__dirname, '/public/routes.jsx') + routesFilePath: join(__dirname, '/public/routes.jsx'), + performanceCollector: function(stats) { + console.log(stats); + } }); // set the engine diff --git a/package.json b/package.json index 45de132..383d94d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-engine", - "version": "3.0.0-rc.2", + "version": "3.0.0", "description": "a composite render engine for express apps to render both plain react views and react-router views", "main": "index.js", "scripts": { @@ -20,7 +20,6 @@ "glob": "^5.0.3", "history": "^1.12.2", "lodash": "^3.10.1", - "lodash-compat": "^3.10.1", "parent-require": "^1.0.0", "react-dom": "^0.14.0" }, From fd383548d2916d2b5e7a59685be0a46362f8bdb3 Mon Sep 17 00:00:00 2001 From: "Selvanathan, Sam" Date: Fri, 8 Jan 2016 16:13:15 -0800 Subject: [PATCH 27/28] fix the code review comments --- README.md | 2 +- example/server.js | 2 +- lib/client.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 557e719..48de53c 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ The options object can contain properties from [react router's create configurat Additionally, it can contain the following **optional** properties, - `docType`: \ - a string that can be used as a doctype (_Default: ``_). - (docType might not make sense if you are rendering partials/sub page components, in that case you can pass and empty string as docType) + (docType might not make sense if you are rendering partials/sub page components, in that case you can pass an empty string as docType) - `routesFilePath`: \ - path for the file that contains the react router routes. react-engine uses this behind the scenes to reload the routes file in 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. diff --git a/example/server.js b/example/server.js index c9f83dd..30ebd48 100644 --- a/example/server.js +++ b/example/server.js @@ -52,7 +52,7 @@ app.use(express.static(join(__dirname, '/public'))); app.use(favicon(join(__dirname, '/public/favicon.ico'))); -// add the our app routes +// add our app routes app.get('*', function(req, res) { res.render(req.url, { movies: movies diff --git a/lib/client.js b/lib/client.js index bf32945..e79d32b 100644 --- a/lib/client.js +++ b/lib/client.js @@ -77,7 +77,7 @@ exports.boot = function boot(options, callback) { routes: options.routes, history: routerHistory.createHistory() }); - ReactDOM.render(routerComponent, _document); + ReactDOM.render(routerComponent, mountNode); } else { // get the file from viewResolver supplying it with a view name var view = viewResolver(props.__meta.view); @@ -88,7 +88,7 @@ exports.boot = function boot(options, callback) { // render the factory on the client // doing this, sets up the event // listeners and stuff aka mounting views. - ReactDOM.render(viewFactory(props), _document); + ReactDOM.render(viewFactory(props), mountNode); } // call the callback with the data that was used for rendering From fa82bb6c1967cec369329779480ae9ceb39e4932 Mon Sep 17 00:00:00 2001 From: "Selvanathan, Sam" Date: Fri, 8 Jan 2016 16:15:53 -0800 Subject: [PATCH 28/28] fix the typo in ready --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 48de53c..1fb489a 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ Note: By default, the following three keys are always filtered out from `renderO ### Handling redirects and route not found errors on the server side 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/rackt/react-router/blob/5590516ec228765cbb176c81fb15fe1d4662e475/docs/guides/advanced/ServerRendering.md). -To handle the above during the lifecycle of a request, add a error type check in your express error middleware. The following are the three types of error that get thrown by react-engine: +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: Error Type | Description -------------------- | --------------------------------------------------------