I was hacking my custom middleware into the webpack-dev-server like this:
// 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
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)
})