UNPKG

dev-toolkit

Version:

Universal Development Toolkit for React Veterans

110 lines (97 loc) 4.31 kB
import express from 'express'; import expressHandlebars from 'express-handlebars'; import path from 'path'; import fs from 'fs'; import React from 'react'; import { renderToString } from 'react-dom/server'; import clearModule from 'clear-module'; import { isDev, isProd, usePreRender } from 'dev-toolkit/settings'; // Unlike the client app, the server app can only ever be run in Node.js // we therefore have direct access to Node-specific things like `process` const serverPort = process.env.SERVER_PORT || 3000; const projectDirectory = process.cwd(); const clientFolder = path.resolve(projectDirectory, 'src/client'); const serverViews = path.resolve(projectDirectory, 'src/server/views'); const rootComponentPath = path.resolve(clientFolder, 'RootComponent'); export default new class { constructor() { // Let dev-toolkit know about express by setting `this.express`, // this allows dev-toolkit to attach the dev-server middleware to webpack this.express = express(); // Handlebars is used for server-rendering the html template in `src/server/views` this.handlebarsInstance = expressHandlebars.create(); // Use Handlebars as the view engine in express this.express.engine('hbs', this.handlebarsInstance.engine); this.express.set('views', serverViews).set('view engine', 'hbs'); // Prevent express from sending powered-by header this.express.disable('x-powered-by'); } // Ability to launch server later (allows dev-toolkit to bind webpack-middleware before start) start({ assets, buildFolder }) { // Provide a simple health-check endpoint to see if the server is alive this.express.get('/health', (req, res) => res.send('OK')); // Make assets in build folder available to the client. // In development, the `webpack-dev-middleware` used by dev-toolkit takes care of this. if (!isDev) { this.express.use(express.static(buildFolder)); } // By default, dev-toolkit serves the build folder with pre-rendered files. if (isDev || (isProd && !usePreRender)) { // Render the template-file on any incoming requests this.express.use((req, res) => { // Remove Client App from cache (cheap server-side Hot-Reload) if (isDev) { // NOTE: We need to explicitly clear all the modules in the client directory. // It's a nice to have. Not guaranteed to always work, take it with a grain of salt. clearModule.match(new RegExp(`^${clientFolder}`, 'i')); } import(rootComponentPath).then(module => { try { // Load newest version of Client App via RootComponent const RootComponent = module.default; res.status(200).render('template', { assets, renderedHtml: renderToString(<RootComponent />), }); } catch (error) { // log any rendering or script errors console.log(error.message); } }); }); } // Run the express server by listening on the specified port this.serverInstance = this.express.listen(serverPort, () => { // eslint-disable-next-line no-console console.log(`Server is listening on port ${serverPort}`); }); } // A way to stop and shut-down the server, you might need this for things like feature-tests stop() { this.serverInstance.close(); } // Rendering of the html on build happens through this preRender-method preRender({ assets, buildFolder }) { // return a Promise to dev-toolkit return new Promise((resolve, reject) => { // Load Client App via RootComponent import(rootComponentPath).then(module => { const RootComponent = module.default; // Here handlebars is used to generate the html without express and without webpack this.handlebarsInstance .render(path.join(serverViews, 'template.hbs'), { assets, renderedHtml: renderToString(<RootComponent />), }) .then(html => { // Generated html is written to html file in build folder fs.writeFile( path.join(buildFolder, 'index.html'), html, error => (error ? reject(error) : resolve()) ); }); }); }); } }();