middleware.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. module.exports = webpackHotMiddleware;
  2. var helpers = require('./helpers');
  3. var pathMatch = helpers.pathMatch;
  4. function webpackHotMiddleware(compiler, opts) {
  5. opts = opts || {};
  6. opts.log =
  7. typeof opts.log == 'undefined' ? console.log.bind(console) : opts.log;
  8. opts.path = opts.path || '/__webpack_hmr';
  9. opts.heartbeat = opts.heartbeat || 10 * 1000;
  10. opts.statsOptions =
  11. typeof opts.statsOptions == 'undefined' ? {} : opts.statsOptions;
  12. var eventStream = createEventStream(opts.heartbeat);
  13. var latestStats = null;
  14. var closed = false;
  15. if (compiler.hooks) {
  16. compiler.hooks.invalid.tap('webpack-hot-middleware', onInvalid);
  17. compiler.hooks.done.tap('webpack-hot-middleware', onDone);
  18. } else {
  19. compiler.plugin('invalid', onInvalid);
  20. compiler.plugin('done', onDone);
  21. }
  22. function onInvalid() {
  23. if (closed) return;
  24. latestStats = null;
  25. if (opts.log) opts.log('webpack building...');
  26. eventStream.publish({ action: 'building' });
  27. }
  28. function onDone(statsResult) {
  29. if (closed) return;
  30. // Keep hold of latest stats so they can be propagated to new clients
  31. latestStats = statsResult;
  32. publishStats(
  33. 'built',
  34. latestStats,
  35. eventStream,
  36. opts.log,
  37. opts.statsOptions
  38. );
  39. }
  40. var middleware = function (req, res, next) {
  41. if (closed) return next();
  42. if (!pathMatch(req.url, opts.path)) return next();
  43. eventStream.handler(req, res);
  44. if (latestStats) {
  45. // Explicitly not passing in `log` fn as we don't want to log again on
  46. // the server
  47. publishStats('sync', latestStats, eventStream, false, opts.statsOptions);
  48. }
  49. };
  50. middleware.publish = function (payload) {
  51. if (closed) return;
  52. eventStream.publish(payload);
  53. };
  54. middleware.close = function () {
  55. if (closed) return;
  56. // Can't remove compiler plugins, so we just set a flag and noop if closed
  57. // https://github.com/webpack/tapable/issues/32#issuecomment-350644466
  58. closed = true;
  59. eventStream.close();
  60. eventStream = null;
  61. };
  62. return middleware;
  63. }
  64. function createEventStream(heartbeat) {
  65. var clientId = 0;
  66. var clients = {};
  67. function everyClient(fn) {
  68. Object.keys(clients).forEach(function (id) {
  69. fn(clients[id]);
  70. });
  71. }
  72. var interval = setInterval(function heartbeatTick() {
  73. everyClient(function (client) {
  74. client.write('data: \uD83D\uDC93\n\n');
  75. });
  76. }, heartbeat).unref();
  77. return {
  78. close: function () {
  79. clearInterval(interval);
  80. everyClient(function (client) {
  81. if (!client.finished) client.end();
  82. });
  83. clients = {};
  84. },
  85. handler: function (req, res) {
  86. var headers = {
  87. 'Access-Control-Allow-Origin': '*',
  88. 'Content-Type': 'text/event-stream;charset=utf-8',
  89. 'Cache-Control': 'no-cache, no-transform',
  90. // While behind nginx, event stream should not be buffered:
  91. // http://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffering
  92. 'X-Accel-Buffering': 'no',
  93. };
  94. var isHttp1 = !(parseInt(req.httpVersion) >= 2);
  95. if (isHttp1) {
  96. req.socket.setKeepAlive(true);
  97. Object.assign(headers, {
  98. Connection: 'keep-alive',
  99. });
  100. }
  101. res.writeHead(200, headers);
  102. res.write('\n');
  103. var id = clientId++;
  104. clients[id] = res;
  105. req.on('close', function () {
  106. if (!res.finished) res.end();
  107. delete clients[id];
  108. });
  109. },
  110. publish: function (payload) {
  111. everyClient(function (client) {
  112. client.write('data: ' + JSON.stringify(payload) + '\n\n');
  113. });
  114. },
  115. };
  116. }
  117. function publishStats(action, statsResult, eventStream, log, statsOptions) {
  118. var resultStatsOptions = Object.assign(
  119. {
  120. all: false,
  121. cached: true,
  122. children: true,
  123. modules: true,
  124. timings: true,
  125. hash: true,
  126. errors: true,
  127. warnings: true,
  128. },
  129. statsOptions
  130. );
  131. var bundles = [];
  132. // multi-compiler stats have stats for each child compiler
  133. // see https://github.com/webpack/webpack/blob/main/lib/MultiCompiler.js#L97
  134. if (statsResult.stats) {
  135. var processed = statsResult.stats.map(function (stats) {
  136. return extractBundles(normalizeStats(stats, resultStatsOptions));
  137. });
  138. bundles = processed.flat();
  139. } else {
  140. bundles = extractBundles(normalizeStats(statsResult, resultStatsOptions));
  141. }
  142. bundles.forEach(function (stats) {
  143. var name = stats.name || '';
  144. // Fallback to compilation name in case of 1 bundle (if it exists)
  145. if (!name && stats.compilation) {
  146. name = stats.compilation.name || '';
  147. }
  148. if (log) {
  149. log(
  150. 'webpack built ' +
  151. (name ? name + ' ' : '') +
  152. stats.hash +
  153. ' in ' +
  154. stats.time +
  155. 'ms'
  156. );
  157. }
  158. eventStream.publish({
  159. name: name,
  160. action: action,
  161. time: stats.time,
  162. hash: stats.hash,
  163. warnings: formatErrors(stats.warnings || []),
  164. errors: formatErrors(stats.errors || []),
  165. modules: buildModuleMap(stats.modules),
  166. });
  167. });
  168. }
  169. function formatErrors(errors) {
  170. if (!errors || !errors.length) {
  171. return [];
  172. }
  173. if (typeof errors[0] === 'string') {
  174. return errors;
  175. }
  176. // Convert webpack@5 error info into a backwards-compatible flat string
  177. return errors.map(function (error) {
  178. var moduleName = error.moduleName || '';
  179. var loc = error.loc || '';
  180. return moduleName + ' ' + loc + '\n' + error.message;
  181. });
  182. }
  183. function normalizeStats(stats, statsOptions) {
  184. var statsJson = stats.toJson(statsOptions);
  185. if (stats.compilation) {
  186. // webpack 5 has the compilation property directly on stats object
  187. Object.assign(statsJson, {
  188. compilation: stats.compilation,
  189. });
  190. }
  191. return statsJson;
  192. }
  193. function extractBundles(stats) {
  194. // Stats has modules, single bundle
  195. if (stats.modules) return [stats];
  196. // Stats has children, multiple bundles
  197. if (stats.children && stats.children.length) return stats.children;
  198. // Not sure, assume single
  199. return [stats];
  200. }
  201. function buildModuleMap(modules) {
  202. var map = {};
  203. modules.forEach(function (module) {
  204. map[module.id] = module.name;
  205. });
  206. return map;
  207. }