module.exports = webpackHotMiddleware; var helpers = require('./helpers'); var pathMatch = helpers.pathMatch; function webpackHotMiddleware(compiler, opts) { opts = opts || {}; opts.log = typeof opts.log == 'undefined' ? console.log.bind(console) : opts.log; opts.path = opts.path || '/__webpack_hmr'; opts.heartbeat = opts.heartbeat || 10 * 1000; opts.statsOptions = typeof opts.statsOptions == 'undefined' ? {} : opts.statsOptions; var eventStream = createEventStream(opts.heartbeat); var latestStats = null; var closed = false; if (compiler.hooks) { compiler.hooks.invalid.tap('webpack-hot-middleware', onInvalid); compiler.hooks.done.tap('webpack-hot-middleware', onDone); } else { compiler.plugin('invalid', onInvalid); compiler.plugin('done', onDone); } function onInvalid() { if (closed) return; latestStats = null; if (opts.log) opts.log('webpack building...'); eventStream.publish({ action: 'building' }); } function onDone(statsResult) { if (closed) return; // Keep hold of latest stats so they can be propagated to new clients latestStats = statsResult; publishStats( 'built', latestStats, eventStream, opts.log, opts.statsOptions ); } var middleware = function (req, res, next) { if (closed) return next(); if (!pathMatch(req.url, opts.path)) return next(); eventStream.handler(req, res); if (latestStats) { // Explicitly not passing in `log` fn as we don't want to log again on // the server publishStats('sync', latestStats, eventStream, false, opts.statsOptions); } }; middleware.publish = function (payload) { if (closed) return; eventStream.publish(payload); }; middleware.close = function () { if (closed) return; // Can't remove compiler plugins, so we just set a flag and noop if closed // https://github.com/webpack/tapable/issues/32#issuecomment-350644466 closed = true; eventStream.close(); eventStream = null; }; return middleware; } function createEventStream(heartbeat) { var clientId = 0; var clients = {}; function everyClient(fn) { Object.keys(clients).forEach(function (id) { fn(clients[id]); }); } var interval = setInterval(function heartbeatTick() { everyClient(function (client) { client.write('data: \uD83D\uDC93\n\n'); }); }, heartbeat).unref(); return { close: function () { clearInterval(interval); everyClient(function (client) { if (!client.finished) client.end(); }); clients = {}; }, handler: function (req, res) { var headers = { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'text/event-stream;charset=utf-8', 'Cache-Control': 'no-cache, no-transform', // While behind nginx, event stream should not be buffered: // http://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffering 'X-Accel-Buffering': 'no', }; var isHttp1 = !(parseInt(req.httpVersion) >= 2); if (isHttp1) { req.socket.setKeepAlive(true); Object.assign(headers, { Connection: 'keep-alive', }); } res.writeHead(200, headers); res.write('\n'); var id = clientId++; clients[id] = res; req.on('close', function () { if (!res.finished) res.end(); delete clients[id]; }); }, publish: function (payload) { everyClient(function (client) { client.write('data: ' + JSON.stringify(payload) + '\n\n'); }); }, }; } function publishStats(action, statsResult, eventStream, log, statsOptions) { var resultStatsOptions = Object.assign( { all: false, cached: true, children: true, modules: true, timings: true, hash: true, errors: true, warnings: true, }, statsOptions ); var bundles = []; // multi-compiler stats have stats for each child compiler // see https://github.com/webpack/webpack/blob/main/lib/MultiCompiler.js#L97 if (statsResult.stats) { var processed = statsResult.stats.map(function (stats) { return extractBundles(normalizeStats(stats, resultStatsOptions)); }); bundles = processed.flat(); } else { bundles = extractBundles(normalizeStats(statsResult, resultStatsOptions)); } bundles.forEach(function (stats) { var name = stats.name || ''; // Fallback to compilation name in case of 1 bundle (if it exists) if (!name && stats.compilation) { name = stats.compilation.name || ''; } if (log) { log( 'webpack built ' + (name ? name + ' ' : '') + stats.hash + ' in ' + stats.time + 'ms' ); } eventStream.publish({ name: name, action: action, time: stats.time, hash: stats.hash, warnings: formatErrors(stats.warnings || []), errors: formatErrors(stats.errors || []), modules: buildModuleMap(stats.modules), }); }); } function formatErrors(errors) { if (!errors || !errors.length) { return []; } if (typeof errors[0] === 'string') { return errors; } // Convert webpack@5 error info into a backwards-compatible flat string return errors.map(function (error) { var moduleName = error.moduleName || ''; var loc = error.loc || ''; return moduleName + ' ' + loc + '\n' + error.message; }); } function normalizeStats(stats, statsOptions) { var statsJson = stats.toJson(statsOptions); if (stats.compilation) { // webpack 5 has the compilation property directly on stats object Object.assign(statsJson, { compilation: stats.compilation, }); } return statsJson; } function extractBundles(stats) { // Stats has modules, single bundle if (stats.modules) return [stats]; // Stats has children, multiple bundles if (stats.children && stats.children.length) return stats.children; // Not sure, assume single return [stats]; } function buildModuleMap(modules) { var map = {}; modules.forEach(function (module) { map[module.id] = module.name; }); return map; }