Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

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

This comment has been minimized.

Copy link

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
You can’t perform that action at this time.