diff --git a/README.md b/README.md index 1560cd9..1fb489a 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,9 @@ ### 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 +# 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 ``` ### Usage On Server Side @@ -75,13 +76,13 @@ The options object can contain properties from [react router's create configurat Additionally, it can contain the following **optional** properties, -- `docType`: _String_ - 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 should pass an empty string as docType. -- `routesFilePath`: _String_ - path for the file that contains the react router routes. +- `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 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. -- `renderOptionsKeysToFilter`: _Array_ - 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`: _Function_ - 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) ###### Rendering views on server side ```js @@ -150,6 +151,43 @@ 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 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 +-------------------- | -------------------------------------------------------- +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 MATCH_REDIRECT error, `redirectLocation` property of the err has the new redirection location_ + +```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). @@ -174,7 +212,7 @@ function collector(stats) { } var engine = require('react-engine').server.create({ - reactRoutes: './routes.jsx' + routes: './routes.jsx' performanceCollector: collector }); ``` @@ -185,6 +223,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/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 bdc3bea..2096a75 100644 --- a/example/package.json +++ b/example/package.json @@ -9,13 +9,16 @@ "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", + "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-rc.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 0000000..78713f9 Binary files /dev/null and b/example/public/favicon.ico differ diff --git a/example/public/routes.jsx b/example/public/routes.jsx index 4c97770..5bcaebe 100644 --- a/example/public/routes.jsx +++ b/example/public/routes.jsx @@ -15,17 +15,19 @@ 'use strict'; -// import react and react-router -var React = require('react'); -var Router = require('react-router'); +import React from 'react'; +import { Router, Route, IndexRoute, Redirect } 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/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..d20063a 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({ @@ -31,11 +30,11 @@ 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..eef4fc8 100644 --- a/example/public/views/list.jsx +++ b/example/public/views/list.jsx @@ -29,8 +29,8 @@ 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..30ebd48 --- /dev/null +++ b/example/server.js @@ -0,0 +1,85 @@ +/*-------------------------------------------------------------------------------------------------------------------*\ +| 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; + +import {join} from 'path'; +import express from 'express'; +import favicon from 'serve-favicon'; +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 = ReactEngine.server.create({ + routes: routes, + routesFilePath: join(__dirname, '/public/routes.jsx'), + performanceCollector: function(stats) { + console.log(stats); + } +}); + +// 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', ReactEngine.expressView); + +// expose public folder as static assets +app.use(express.static(join(__dirname, '/public'))); + +app.use(favicon(join(__dirname, '/public/favicon.ico'))); + +// add our app routes +app.get('*', function(req, res) { + res.render(req.url, { + movies: movies + }); +}); + +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/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/index.js b/index.js index 157a279..16cf1fd 100644 --- a/index.js +++ b/index.js @@ -18,3 +18,4 @@ exports.server = require('./lib/server'); exports.client = require('./lib/client'); exports.expressView = require('./lib/expressView'); +exports.reactRouterServerErrors = require('./lib/reactRouterServerErrors'); diff --git a/lib/client.js b/lib/client.js index c2a0482..e79d32b 100644 --- a/lib/client.js +++ b/lib/client.js @@ -16,6 +16,10 @@ 'use strict'; 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 @@ -63,22 +67,18 @@ 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; + // 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)); + }, - // create and run the router - 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 - React.render(componentInstance, mountNode); + routes: options.routes, + history: routerHistory.createHistory() }); - } - else { + 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. - React.render(viewFactory(props), mountNode); + ReactDOM.render(viewFactory(props), mountNode); } // call the callback with the data that was used for rendering diff --git a/lib/expressView.js b/lib/expressView.js index b0cb541..803f154 100644 --- a/lib/expressView.js +++ b/lib/expressView.js @@ -45,8 +45,7 @@ ReactEngineView.prototype.lookup = function lookup(name) { debug(format('ReactEngineView :lookup: name: %s', name)); if (this.useRouter) { return name; - } - else { + } else { return View.prototype.lookup.call(this, name); } }; @@ -55,8 +54,7 @@ ReactEngineView.prototype.render = function render(options, fn) { debug(format('ReactEngineView :render:')); if (this.useRouter) { this.engine(this.name, options, fn); - } - else { + } else { return View.prototype.render.call(this, options, fn); } }; diff --git a/lib/reactRouterServerErrors.js b/lib/reactRouterServerErrors.js new file mode 100644 index 0000000..50d3632 --- /dev/null +++ b/lib/reactRouterServerErrors.js @@ -0,0 +1,41 @@ +/*-------------------------------------------------------------------------------------------------------------------*\ +| 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 errorTypes = ['MATCH_REDIRECT', + 'MATCH_NOT_FOUND', + 'MATCH_INTERNAL_ERROR']; + +var properties = {}; + +errorTypes.map(function(errorType) { + properties[errorType] = { + configurable: false, + writable: false, + enumerable: true, + value: errorType + }; +}); + +/* + 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 dc30861..2c6e031 100644 --- a/lib/server.js +++ b/lib/server.js @@ -19,12 +19,23 @@ var path = require('path'); var util = require('./util'); var assert = require('assert'); 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'); var omit = require('lodash/object/omit'); var merge = require('lodash/object/merge'); var isString = require('lodash/lang/isString'); +// safely require the peer-dependencies +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 = ['' ].join(''); +function generateReactRouterServerError(type, existingErrorObj, additionalProperties) { + var err = existingErrorObj || new Error('react router match fn error'); + err._type = type; + if (additionalProperties) { + merge(err, additionalProperties); + } + + return err; +} + exports.create = function create(createOptions) { // safely require the peer-dependencies @@ -86,6 +107,25 @@ exports.create = function create(createOptions) { callback(err, html); } + function renderAndDecorate(component, data, html) { + // render the component + html += ReactDOMServer.renderToString(component); + + // 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 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. @@ -105,22 +145,37 @@ exports.create = function create(createOptions) { markupId: Config.client.markupId } }, omit(options, createOptions.renderOptionsKeysToFilter)); - if (this.useRouter && !createOptions.routes) { return done(new Error('asking to use react router for rendering, but no routes are provided')); } - var componentInstance; - try { if (this.useRouter) { - // change the `location` property for this render lifecycle - createOptions.location = thing; - var router = Router.create(createOptions); - - // runs the react router that gives the Component to render - router.run(function onRouterRun(Component) { - componentInstance = React.createElement(Component, data); + return match({ routes:createOptions.routes, location:thing}, function reactRouterMatchHandler(error, redirectLocation, renderProps) { + if (error) { + debug('server.js match 500 %s', 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 = 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" + 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)); + } else { + debug('server.js match 404'); + var err = generateReactRouterServerError(ReactRouterServerErrors.MATCH_NOT_FOUND); + return done(err); + } }); } else { @@ -138,26 +193,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), data, html)); } - - // 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 9abddcf..383d94d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-engine", - "version": "2.6.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": { @@ -18,10 +18,15 @@ "dependencies": { "debug": "^2.1.3", "glob": "^5.0.3", + "history": "^1.12.2", "lodash": "^3.10.1", - "parent-require": "^1.0.0" + "parent-require": "^1.0.0", + "react-dom": "^0.14.0" }, "devDependencies": { + "babel-core": "^6.3.26", + "babel-preset-react": "^6.3.13", + "babel-register": "^6.3.13", "cheerio": "^0.19.0", "esprima-fb": "^14001.1.0-dev-harmony-fb", "express": "^4.12", @@ -34,8 +39,8 @@ "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.3", "rewire": "^2.3.1", "sinon": "^1.14.1", "tape": "^3.5.0", diff --git a/test/client.js b/test/client.js index e261f94..5a3269d 100644 --- a/test/client.js +++ b/test/client.js @@ -20,7 +20,7 @@ var rewire = require('rewire'); var jsdom = require('jsdom').jsdom; var DATA_MODEL = require('./server').DATA_MODEL; var DATA_MODEL_PROPS = Object.keys(DATA_MODEL); -var assertions = require('./fixtures/assertions'); +var assertions = require('./fixtures/assertions.json'); // boot options var options = { diff --git a/test/fixtures/assertions.json b/test/fixtures/assertions.json index 16c570e..af03b28 100644 --- a/test/fixtures/assertions.json +++ b/test/fixtures/assertions.json @@ -1,5 +1,5 @@ { - "PROFILE_OUTPUT": "Hello, world!

    Joshua

    ", - "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 50b3ec4..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.Route, - { path: '/' }, - React.createElement( - Router.Route, - { path: 'account', handler: App }, - React.createElement(Router.DefaultRoute, { name: 'account', handler: Account }) - ) +var routes = module.exports = ( + + + + + + ); diff --git a/test/fixtures/views/layout.js b/test/fixtures/views/account.jsx similarity index 83% rename from test/fixtures/views/layout.js rename to test/fixtures/views/account.jsx index 59387a0..e0f1735 100644 --- a/test/fixtures/views/layout.js +++ b/test/fixtures/views/account.jsx @@ -19,18 +19,11 @@ var React = require('react'); module.exports = React.createClass({ - displayName: 'layout', - - render: function render() { - - return React.createElement( - 'html', null, - React.createElement( - 'head', null, - React.createElement('meta', { charSet: 'utf-8' }), - React.createElement('title', null, this.props.title) - ), - React.createElement('body', null, this.props.children) + 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 446e137..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.createElement(Router.RouteHandler, this.props) + return ( + + {this.props.children} + ); } }); diff --git a/test/fixtures/views/profile.js b/test/fixtures/views/layout.jsx similarity index 86% rename from test/fixtures/views/profile.js rename to test/fixtures/views/layout.jsx index 82554c7..229464f 100644 --- a/test/fixtures/views/profile.js +++ b/test/fixtures/views/layout.jsx @@ -16,20 +16,21 @@ 'use strict'; var React = require('react'); -var Layout = require('./layout'); module.exports = React.createClass({ - displayName: 'profile', + render: function() { - render: function render() { - - return React.createElement(Layout, this.props, - React.createElement('div', { id: 'profile' }, - React.createElement( - 'h1', null, this.props.name - ) - ) + return ( + + + + {this.props.title || 'Hello, world!'} + + {this.props.children} + + + ); } }); diff --git a/test/fixtures/views/account.js b/test/fixtures/views/profile.jsx similarity index 90% rename from test/fixtures/views/account.js rename to test/fixtures/views/profile.jsx index fd4be86..b02993a 100644 --- a/test/fixtures/views/account.js +++ b/test/fixtures/views/profile.jsx @@ -16,21 +16,18 @@ 'use strict'; var React = require('react'); +var Layout = require('./layout.jsx'); module.exports = React.createClass({ - displayName: 'account', - render: function render() { - return React.createElement( - 'div', - { id: 'account' }, - 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); +});