233 lines
6.1 KiB
JavaScript
233 lines
6.1 KiB
JavaScript
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;
|
|
}
|