Skip to content

Instantly share code, notes, and snippets.

@heygrady
Created February 11, 2019 07:53
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save heygrady/ae6ec0d725c6c68974802ba5d8328617 to your computer and use it in GitHub Desktop.
Save heygrady/ae6ec0d725c6c68974802ba5d8328617 to your computer and use it in GitHub Desktop.

I was hacking my custom middleware into the webpack-dev-server like this:

start.js

// start.js
// use custom serverSideRender middleware with webpack-dev-server
const serverSideRenderMiddleware = require('./ssr-middleware')

// Copy the defaultFeatures logic from webpack-dev-server. We need to hack the
// default features in order to ensure that our custom serverSideRender
// middleware always runs after contentBaseFiles but before the other default
// features. Running "after" last will result in the public index always being
// rendered (for some reason).
// TODO: is there a better way?
const createDefaultFeatures = options => {
  const { after, contentBase } = options;
  const defaultFeatures = ['before', 'setup', 'headers', 'middleware'];
  if (options.proxy) {
    defaultFeatures.push('proxy', 'middleware');
  }
  if (contentBase !== false) {
    defaultFeatures.push('contentBaseFiles');
  }
  // Ensure "after" runs after "middleware" and "contentBaseFiles" but before everything else
  if (after) {
    defaultFeatures.push('after');
  }
  if (options.watchContentBase) {
    defaultFeatures.push('watchContentBase');
  }
  if (options.historyApiFallback) {
    defaultFeatures.push('historyApiFallback', 'middleware');
    if (contentBase !== false) {
      defaultFeatures.push('contentBaseFiles');
    }
    // Ensure "after" runs after "middleware" and "contentBaseFiles" but before everything else
    if (after) {
      defaultFeatures.push('after');
    }
  }
  defaultFeatures.push('magicHtml');
  // NOTE: contentBaseIndex is the devil 😈. *Never* enable it.
  // if (contentBase !== false) { defaultFeatures.push('contentBaseIndex'); }
  // compress is placed last and uses unshift so that it will be the first middleware used
  if (options.compress) {
    defaultFeatures.unshift('compress');
  }
  return defaultFeatures;
};

let afterCounter = 0;
const serverConfig = {
  // ... whatever settings you like
  serverSideRender: true,
  after(app) {
    if (afterCounter === 0) {
      // This allows us to render the HTML on the server. We don't need this
      // unless we're doing SSR.
      //
      // serverSideRenderMiddleware should be an express middleware function
      // that uses `res.send` to deliver the rendered HTML.
      //
      // You may wish to review the webpack-dev-middleware for more
      // information on how to handle webpackStats in your middleware.
      // https://github.com/webpack/webpack-dev-middleware#server-side-rendering
      app.use(serverSideRenderMiddleware);
    }
    // Because of the way we're abusing after, it appears multiple times in
    // the sequence. Here we increment a counter to better control which after
    // we're trying to target. Above you can see that we want to inject the
    // ssr middleware on the first after. The after callback runs three times.
    afterCounter++;
  },
};

// While webpack-dev-middleware supports a serverSideRender option,
// webpack-dev-server does not. We're passing in the serverSideRender
// option from webpackDevServer.config.js but there's no good way to have
// our reactApp middleware run in the right place. So, we need to override
// the features option to include our "after" middleware in the right spots.
if (serverConfig.serverSideRender) {
  // NOTE: this will break if you're already using the features option.
  if (serverConfig.features) {
    console.warn('The features option has been replaced to enable ssr.');
  }
  serverConfig.features = createDefaultFeatures(serverConfig);
}

// create the dev server
const config = { /* ... your client-side webpack config */ }
const configServer = { /* ... your server-side webpack config */ }
const compiler = webpack([config, configServer]);
const devServer = new WebpackDevServer(compiler, serverConfig);

// start the dev server
const PORT = parseInt(process.env.PORT, 10) || 3000;
const HOST = process.env.HOST || '0.0.0.0';
devServer.listen(PORT, HOST, err => {
  if (err) {
    return console.log(err);
  }
  console.log('Starting the development server');
});

['SIGINT', 'SIGTERM'].forEach(function(sig) {
  process.on(sig, function() {
    devServer.close();
    process.exit();
  });
});

ssr-middleware.js

// ssr-middleware.js
const path = require('path')
const { wrap } = require('async-middleware')

const fs = require('fs')
const { Volume } = require('memfs')
const { Union } = require('unionfs')
const { patchRequire } = 'fs-monkey'

// TODO: not positive these paths are correct (pseudo-code)
const mainPath = path.join(__dirname, '..', 'dist/server/main.js')
const statsPath = path.join(__dirname, '..', 'dist/static/react-loadable.json')
const manifestPath = path.join(__dirname, '..', 'dist/static/asset-manifest.json')

const mountMemoryFileSystem = async (res) => {
  const { stats } = res.locals.webpackStats
  // TODO: is this correct? (psuedo-code)
  const { outputFileSystem: clientFs } = stats[0].compilation.compiler
  const { outputFileSystem: serverFs } = stats[1].compilation.compiler
  const ufs = new Union()
  // allow us to require from all three volumes as if they were one
  ufs.use(clientFs).use(serverFs).use(fs)
  patchRequire(ufs) // <-- memory-fs doesn't support full fs API so this fails unless we use memfs
}

const initMiddleware = async (res) => {
  // the main app exports a createMiddleware function
  const { createMiddleWare, preloadAll } = requireNoCache(mainPath)
  // we also need assets from the client build
  const stats = requireNoCache(statsPath)
  const manifest = requireNoCache(manifestPath)
  // https://github.com/jamiebuilds/react-loadable#preloading-all-your-loadable-components-on-the-server
  await preloadAll()
  const middleware = createMiddleWare(stats, manifest)
  return middleware
}

// https://stackoverflow.com/a/16060619
const requireNoCache = (module) => {
  delete require.cache[require.resolve(module)]
  return require(module)
}

const isMemoryFileSystem = (res) => {
  const stats = res.locals.webpackStats.stats[1]
  const { outputFileSystem } = stats.compilation.compiler
  return outputFileSystem instanceof Volume
}

// We don't really want to continue to the other middleware
const fakeNext = () => undefined

module.exports = wrap(async (req, res, next) => {
  if (isMemoryFileSystem(res)) {
    // TODO: this would remount the filesystem on every request
    // should probably check if that's necessary
    mountMemoryFileSystem(res)
  }
  const middleware = await initMiddleware(res)
  middleware(req, res, fakeNext)
})
@hinell
Copy link

hinell commented Feb 18, 2019

I wonder why don't you use separate simple server for development.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment