middleware.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  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. var eventStream = createEventStream(opts.heartbeat);
  11. var latestStats = null;
  12. var closed = false;
  13. if (compiler.hooks) {
  14. compiler.hooks.invalid.tap('webpack-hot-middleware', onInvalid);
  15. compiler.hooks.done.tap('webpack-hot-middleware', onDone);
  16. } else {
  17. compiler.plugin('invalid', onInvalid);
  18. compiler.plugin('done', onDone);
  19. }
  20. function onInvalid() {
  21. if (closed) return;
  22. latestStats = null;
  23. if (opts.log) opts.log('webpack building...');
  24. eventStream.publish({ action: 'building' });
  25. }
  26. function onDone(statsResult) {
  27. if (closed) return;
  28. // Keep hold of latest stats so they can be propagated to new clients
  29. latestStats = statsResult;
  30. publishStats('built', latestStats, eventStream, opts.log);
  31. }
  32. var middleware = function (req, res, next) {
  33. if (closed) return next();
  34. if (!pathMatch(req.url, opts.path)) return next();
  35. eventStream.handler(req, res);
  36. if (latestStats) {
  37. // Explicitly not passing in `log` fn as we don't want to log again on
  38. // the server
  39. publishStats('sync', latestStats, eventStream);
  40. }
  41. };
  42. middleware.publish = function (payload) {
  43. if (closed) return;
  44. eventStream.publish(payload);
  45. };
  46. middleware.close = function () {
  47. if (closed) return;
  48. // Can't remove compiler plugins, so we just set a flag and noop if closed
  49. // https://github.com/webpack/tapable/issues/32#issuecomment-350644466
  50. closed = true;
  51. eventStream.close();
  52. eventStream = null;
  53. };
  54. return middleware;
  55. }
  56. function createEventStream(heartbeat) {
  57. var clientId = 0;
  58. var clients = {};
  59. function everyClient(fn) {
  60. Object.keys(clients).forEach(function (id) {
  61. fn(clients[id]);
  62. });
  63. }
  64. var interval = setInterval(function heartbeatTick() {
  65. everyClient(function (client) {
  66. client.write('data: \uD83D\uDC93\n\n');
  67. });
  68. }, heartbeat).unref();
  69. return {
  70. close: function () {
  71. clearInterval(interval);
  72. everyClient(function (client) {
  73. if (!client.finished) client.end();
  74. });
  75. clients = {};
  76. },
  77. handler: function (req, res) {
  78. var headers = {
  79. 'Access-Control-Allow-Origin': '*',
  80. 'Content-Type': 'text/event-stream;charset=utf-8',
  81. 'Cache-Control': 'no-cache, no-transform',
  82. // While behind nginx, event stream should not be buffered:
  83. // http://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffering
  84. 'X-Accel-Buffering': 'no',
  85. };
  86. var isHttp1 = !(parseInt(req.httpVersion) >= 2);
  87. if (isHttp1) {
  88. req.socket.setKeepAlive(true);
  89. Object.assign(headers, {
  90. Connection: 'keep-alive',
  91. });
  92. }
  93. res.writeHead(200, headers);
  94. res.write('\n');
  95. var id = clientId++;
  96. clients[id] = res;
  97. req.on('close', function () {
  98. if (!res.finished) res.end();
  99. delete clients[id];
  100. });
  101. },
  102. publish: function (payload) {
  103. everyClient(function (client) {
  104. client.write('data: ' + JSON.stringify(payload) + '\n\n');
  105. });
  106. },
  107. };
  108. }
  109. function publishStats(action, statsResult, eventStream, log) {
  110. var stats = statsResult.toJson({
  111. all: false,
  112. cached: true,
  113. children: true,
  114. modules: true,
  115. timings: true,
  116. hash: true,
  117. });
  118. // For multi-compiler, stats will be an object with a 'children' array of stats
  119. var bundles = extractBundles(stats);
  120. bundles.forEach(function (stats) {
  121. var name = stats.name || '';
  122. // Fallback to compilation name in case of 1 bundle (if it exists)
  123. if (bundles.length === 1 && !name && statsResult.compilation) {
  124. name = statsResult.compilation.name || '';
  125. }
  126. if (log) {
  127. log(
  128. 'webpack built ' +
  129. (name ? name + ' ' : '') +
  130. stats.hash +
  131. ' in ' +
  132. stats.time +
  133. 'ms'
  134. );
  135. }
  136. eventStream.publish({
  137. name: name,
  138. action: action,
  139. time: stats.time,
  140. hash: stats.hash,
  141. warnings: stats.warnings || [],
  142. errors: stats.errors || [],
  143. modules: buildModuleMap(stats.modules),
  144. });
  145. });
  146. }
  147. function extractBundles(stats) {
  148. // Stats has modules, single bundle
  149. if (stats.modules) return [stats];
  150. // Stats has children, multiple bundles
  151. if (stats.children && stats.children.length) return stats.children;
  152. // Not sure, assume single
  153. return [stats];
  154. }
  155. function buildModuleMap(modules) {
  156. var map = {};
  157. modules.forEach(function (module) {
  158. map[module.id] = module.name;
  159. });
  160. return map;
  161. }