Last active
May 26, 2016 21:35
-
-
Save iest/ea298ae394620afb3df65d864cbaa91f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
require('babel/register'); // transform required modules | |
const fs = require('fs'); | |
const path = require('path'); | |
const jade = require('jade'); | |
const React = require('react'); | |
const debug = require('debug'); | |
const Helmet = require('react-helmet'); | |
const find = require('lodash/collection').find; | |
const Provider = require('react-redux').Provider; | |
const BodyClassName = require('react-body-classname'); | |
const router = require('react-router'); | |
const RoutingContext = router.RoutingContext; | |
const match = router.match; | |
const errorClient = require('../errorClient'); | |
const COOKIE_NAME = require('../../app/constants').COOKIE_NAME; | |
const REDIRECT_COOKIE = require('../../app/constants').REDIRECT_COOKIE; | |
const serialise = require('../../app/util/serialise'); | |
const fetchAllComponentData = require('../../app/util/fetchAllComponentData'); | |
const serverBundle = require('../build/server-build.bundle'); | |
const createRoutes = serverBundle.createRoutes; | |
const setStore = serverBundle.setStore; | |
const MyNavProxy = serverBundle.MyNavProxy; | |
const MySidebarProxy = serverBundle.MySidebarProxy; | |
const MyTitleBarProxy = serverBundle.MyTitleBarProxy; | |
const config = require('../../app/config'); | |
const isDevelopment = config.NODE_ENV === 'development'; | |
const API_BASE = config.API_BASE; | |
module.exports = () => { | |
return function* appRender() { | |
const token = this.cookies.get(COOKIE_NAME) || null; | |
const store = setStore({token}); | |
const routes = createRoutes(store); | |
try { | |
const output = yield new Promise((resolve) => { | |
match({routes, location: this.url}, (_error, _redirectLocation, _renderProps) => { | |
return resolve({ | |
routeError: _error, | |
redirectLocation: _redirectLocation, | |
renderProps: _renderProps, | |
}); | |
}); | |
}); | |
const routeError = output.routeError; | |
const redirectLocation = output.redirectLocation; | |
const renderProps = output.renderProps; | |
if (routeError) { | |
debug('appRender')('Route error'); | |
return this.throw(500, routeError.message); | |
} | |
// Check to see if we've internally redirected first | |
if (redirectLocation) { | |
debug('appRender')('Transition cancelled. Redirecting...'); | |
const transition = store.getState().global.storedTransition; | |
if (transition) { | |
this.cookies.set(REDIRECT_COOKIE, serialise(transition), { | |
httpOnly: false, | |
}); | |
} | |
return this.redirect(redirectLocation.pathname + redirectLocation.search); | |
} | |
// This shouldn't ever happen as we have a NotFound route... | |
if (renderProps === null) { | |
return this.throw(404, 'Not found'); | |
} | |
// Call all async fetchData statics | |
try { | |
yield fetchAllComponentData( | |
renderProps.components, | |
{ | |
dispatch: store.dispatch, | |
base: API_BASE, | |
token, | |
} | |
).then(actions => { | |
const errors = actions.filter(action => action.error); | |
const did401 = find(errors, errored => errored.payload && errored.payload.status === 401); | |
const did500 = find(errors, errored => errored.payload && errored.payload.status === 500); | |
if (did401) { | |
this.cookies.set(COOKIE_NAME); // set outbound header to delete cookie | |
return this.redirect('/login'); | |
} | |
if (did500) { | |
errorClient.captureException(did500); | |
return this.redirect('/oops'); | |
} | |
}); | |
} catch (error) { | |
debug('appRender')(`fetchAllComponentData failed: "${error}"\n${error.stack}`); | |
} | |
// Check if we have the NotFound route present | |
const notFound = renderProps.components | |
.filter(component => component.isNotFound) | |
.length > 0; | |
if (notFound) { | |
if (this.accepts('html')) { | |
this.status = 404; | |
} else { | |
return null; | |
} | |
} | |
// Pull asset paths out of the manifest | |
let assets; | |
if (isDevelopment) { | |
// Reload it on dev | |
assets = fs.readFileSync(path.resolve(__dirname, '../build/webpack-manifest.json')); | |
assets = JSON.parse(assets); | |
} else { | |
// Cache on production | |
assets = require('../build/webpack-manifest.json'); | |
} | |
const appString = React.renderToString( | |
React.createElement( | |
Provider, | |
{store}, | |
() => | |
React.createElement( | |
RoutingContext, | |
Object.assign({}, renderProps) | |
) | |
) | |
); | |
[ | |
MyNavProxy, | |
MyTitleBarProxy, | |
MySidebarProxy, | |
].forEach(sideEffect => | |
sideEffect.rewind() | |
); | |
const title = Helmet.rewind().title; | |
const bodyClassNames = BodyClassName.rewind(); | |
const appState = serialise(store.getState()); | |
debug('appRender')('rendering index.jade'); | |
this.body = jade.renderFile( | |
`${__dirname}/../index.jade`, | |
{ | |
title, | |
config, | |
bodyClassNames, | |
assets, | |
appString, | |
appState, | |
}); | |
} catch (error) { | |
if (error.redirect) { | |
return this.redirect(error.redirect); | |
} | |
throw error; | |
} | |
}; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Filters out all the compoents that have async `fetchData` calls, wrapped with | |
* `connect` or otherwise, then runs the async methods so we can prepopulate | |
* our store state on the server side render. | |
*/ | |
export default function fetchAllComponentData(components, ...args) { | |
return Promise.all(components | |
.map(component => { | |
if (component.fetchData) { | |
return component.fetchData; | |
} | |
// If we're dealing with a connect-wrapped component | |
if (component.WrappedComponent && component.WrappedComponent.fetchData) { | |
return component.WrappedComponent.fetchData; | |
} | |
}) | |
.filter(method => typeof method === 'function') | |
.map(method => { | |
const result = method(...args); | |
if (typeof result === 'undefined') { | |
throw new Error(`Fetch data should not return ${result}`); | |
} | |
return result; | |
}) | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment