diff --git a/server/app.js b/server/app.js index c7a2cef..ab265ce 100644 --- a/server/app.js +++ b/server/app.js @@ -10,6 +10,8 @@ const path = require('path'); const params = require('express-route-params'); const config = require('./config'); +import loader from './loader'; + const app = express(); params(express); @@ -208,4 +210,45 @@ function ensureAuthenticated(req, res, next) { return next(); } +// SSR + +app.use(compression()); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: false })); +app.use(morgan('dev')); +app.use(cookieParser()); + +// Set up homepage, static assets, and capture everything else +app.use(express.Router().get('/', loader)); +app.use(express.static(path.resolve(__dirname, '../build'))); +app.use(loader); + +// We tell React Loadable to load all required assets and start listening - ROCK AND ROLL! +Loadable.preloadAll().then(() => { + app.listen(PORT, console.log(`App listening on port ${PORT}!`)); +}); + +// Handle the bugs somehow +app.on('error', error => { + if (error.syscall !== 'listen') { + throw error; + } + + const bind = typeof PORT === 'string' ? 'Pipe ' + PORT : 'Port ' + PORT; + + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +}); + + app.listen(3001); diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..968bdfd --- /dev/null +++ b/server/index.js @@ -0,0 +1,18 @@ +const md5File = require('md5-file'); +const path = require('path'); + +const ignoreStyles = require('ignore-styles'); +const register = ignoreStyles.default; + +require("@babel/polyfill"); +require("@babel/register")({ + ignore: [/\/(build|node_modules)\//], + presets: ["@babel/preset-env", "@babel/preset-react"], + plugins: [ + "@babel/plugin-syntax-dynamic-import", + "dynamic-import-node", + "react-loadable/babel" + ] +}); + +require("./app.js"); diff --git a/server/loader.js b/server/loader.js new file mode 100644 index 0000000..fc696da --- /dev/null +++ b/server/loader.js @@ -0,0 +1,96 @@ +import path from 'path'; +import fs from 'fs'; + +import React from 'react'; +import { renderToString } from 'react-dom/server'; +import Helmet from 'react-helmet'; +import { Provider } from 'react-redux'; +import { StaticRouter } from 'react-router'; +import { Frontload, frontloadServerRender } from 'react-frontload'; +import Loadable from 'react-loadable'; + + +import createStore from '../src/store'; +import ExchangeRate from '../src/components/pages/ExchangeRate' + + + +export default (req, res) => { + const injectHTML = (data, { html, title, meta, body, scripts, state }) => { + data = data.replace('', ``); + data = data.replace(/.*?<\/title>/g, title); + data = data.replace('</head>', `${meta}</head>`); + data = data.replace( + '<div id="root"></div>', + `<div id="root">${body}</div><script>window.__PRELOADED_STATE__ = ${state}</script>${scripts.join('')}` + ); + + return data; + }; + + fs.readFile( + path.resolve(__dirname, '../build/index.html'), + 'utf8', + (err, htmlData) => { + if (err) { + console.error('Read error', err); + + return res.status(404).end(); + } + + const { store } = createStore(req.url); + + + const context = {}; + const modules = []; + + frontloadServerRender(() => + renderToString( + <Loadable.Capture report={m => modules.push(m)}> + <Provider store={store}> + <StaticRouter location={req.url} context={context}> + <Frontload isServer={true}> + <ExchangeRate /> + </Frontload> + </StaticRouter> + </Provider> + </Loadable.Capture> + ) + ).then(routeMarkup => { + if (context.url) { + res.writeHead(302, { + Location: context.url + }); + + res.end(); + } else { + + const extractAssets = (assets, chunks) => + Object.keys(assets) + .filter(asset => chunks.indexOf(asset.replace('.js', '')) > -1) + .map(k => assets[k]); + + const extraChunks = extractAssets(manifest, modules).map( + c => `<script type="text/javascript" src="/${c.replace(/^\//, '')}"></script>` + ); + + + const helmet = Helmet.renderStatic(); + + console.log('THE TITLE', helmet.title.toString()); + + const html = injectHTML(htmlData, { + html: helmet.htmlAttributes.toString(), + title: helmet.title.toString(), + meta: helmet.meta.toString(), + body: routeMarkup, + scripts: extraChunks, + state: JSON.stringify(store.getState()).replace(/</g, '\\u003c') + }); + + res.send(html); + } + }); + } + ); +}; \ No newline at end of file