Skip to content

Instantly share code, notes, and snippets.

@iest
Last active May 26, 2016 21:35
Show Gist options
  • Save iest/ea298ae394620afb3df65d864cbaa91f to your computer and use it in GitHub Desktop.
Save iest/ea298ae394620afb3df65d864cbaa91f to your computer and use it in GitHub Desktop.
'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;
}
};
};
/*
* 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