@frontity/core
Version:
The core package of the Frontity framework.
141 lines (115 loc) • 4.03 kB
text/typescript
import path from "path";
import { RequestHandler } from "express";
import { MultiCompiler } from "webpack";
import requireFromString from "require-from-string";
import sourceMapSupport from "source-map-support";
const interopRequireDefault = (obj) => {
return obj && obj.__esModule ? obj.default : obj;
};
const isMultiCompiler = (compiler) => {
// Duck typing as `instanceof MultiCompiler` fails when npm decides to
// install multiple instances of webpack.
return compiler && compiler.compilers;
};
const findCompiler = (multiCompiler, name) => {
return multiCompiler.compilers.filter(
(compiler) => compiler.name.indexOf(name) === 0
);
};
const findStats = (multiStats, name) => {
return multiStats.stats.filter(
(stats) => stats.compilation.name.indexOf(name) === 0
);
};
const getFilename = (serverStats, outputPath, chunkName) => {
const assetsByChunkName = serverStats.toJson().assetsByChunkName;
const filename = assetsByChunkName[chunkName] || "";
// If source maps are generated `assetsByChunkName.main`
// will be an array of filenames.
return path.join(
outputPath,
Array.isArray(filename)
? filename.find((asset) => /\.js$/.test(asset))
: filename
);
};
const getServerRenderer = (filename, buffer) => {
const errMessage = `The 'server' compiler must export a function in the form of \`(options) => (req, res, next) => void\``;
const serverRenderer = interopRequireDefault(
requireFromString(buffer.toString(), filename)
);
if (typeof serverRenderer !== "function") {
throw new Error(errMessage);
}
return serverRenderer;
};
function installSourceMapSupport(fs) {
sourceMapSupport.install({
// NOTE: If https://github.com/evanw/node-source-map-support/pull/149
// lands we can be less aggressive and explicitly invalidate the source
// map cache when Webpack recompiles.
emptyCacheBetweenOperations: true,
retrieveFile(source) {
try {
return fs.readFileSync(source, "utf8");
} catch (ex) {
// Doesn't exist
}
},
});
}
const createConnectHandler = (error, serverRenderer) => (req, res, next) => {
if (error) {
return next(error);
}
serverRenderer(req, res, next);
};
function webpackHotServerMiddleware(
multiCompiler: MultiCompiler
): RequestHandler {
if (!isMultiCompiler(multiCompiler)) {
throw new Error(
`Expected webpack compiler to contain both a 'client' and/or 'server' config`
);
}
const serverCompiler = findCompiler(multiCompiler, "server")[0];
const clientCompilers = findCompiler(multiCompiler, "client");
if (!serverCompiler) {
throw new Error(`Expected a webpack compiler named 'server'`);
}
const outputFs = serverCompiler.outputFileSystem;
const outputPath = serverCompiler.outputPath;
installSourceMapSupport(outputFs);
let serverRenderer;
let error = false;
const doneHandler = (multiStats) => {
error = false;
const serverStats = findStats(multiStats, "server")[0];
// Server compilation errors need to be propagated to the client.
if (serverStats.compilation.errors.length) {
error = serverStats.compilation.errors[0];
return;
}
let clientStatsJson = null;
if (clientCompilers.length) {
const clientStats = findStats(multiStats, "client");
clientStatsJson = clientStats.map((obj) => obj.toJson());
if (clientStatsJson.length === 1) {
clientStatsJson = clientStatsJson[0];
}
}
const filename = getFilename(serverStats, outputPath, "main");
const buffer = outputFs.readFileSync(filename);
try {
serverRenderer = getServerRenderer(filename, buffer);
} catch (ex) {
error = ex;
}
};
multiCompiler.hooks.done.tap("WebpackHotServerMiddleware", doneHandler);
return function () {
// eslint-disable-next-line prefer-spread,prefer-rest-params
return createConnectHandler(error, serverRenderer).apply(null, arguments);
};
}
export default webpackHotServerMiddleware;