lazyCompilationBackend.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. /** @typedef {import("http").IncomingMessage} IncomingMessage */
  7. /** @typedef {import("http").RequestListener} RequestListener */
  8. /** @typedef {import("http").ServerOptions} HttpServerOptions */
  9. /** @typedef {import("http").ServerResponse} ServerResponse */
  10. /** @typedef {import("https").ServerOptions} HttpsServerOptions */
  11. /** @typedef {import("net").AddressInfo} AddressInfo */
  12. /** @typedef {import("net").Server} Server */
  13. /** @typedef {import("../../declarations/WebpackOptions").LazyCompilationDefaultBackendOptions} LazyCompilationDefaultBackendOptions */
  14. /** @typedef {import("../Compiler")} Compiler */
  15. /** @typedef {import("../Module")} Module */
  16. /** @typedef {import("./LazyCompilationPlugin").BackendApi} BackendApi */
  17. /** @typedef {import("./LazyCompilationPlugin").BackendHandler} BackendHandler */
  18. /**
  19. * @param {Omit<LazyCompilationDefaultBackendOptions, "client"> & { client: NonNullable<LazyCompilationDefaultBackendOptions["client"]>}} options additional options for the backend
  20. * @returns {BackendHandler} backend
  21. */
  22. module.exports = options => (compiler, callback) => {
  23. const logger = compiler.getInfrastructureLogger("LazyCompilationBackend");
  24. const activeModules = new Map();
  25. const prefix = "/lazy-compilation-using-";
  26. const isHttps =
  27. options.protocol === "https" ||
  28. (typeof options.server === "object" &&
  29. ("key" in options.server || "pfx" in options.server));
  30. const createServer =
  31. typeof options.server === "function"
  32. ? options.server
  33. : (() => {
  34. const http = isHttps ? require("https") : require("http");
  35. return http.createServer.bind(
  36. http,
  37. /** @type {HttpServerOptions | HttpsServerOptions} */
  38. (options.server)
  39. );
  40. })();
  41. /** @type {function(Server): void} */
  42. const listen =
  43. typeof options.listen === "function"
  44. ? options.listen
  45. : server => {
  46. let listen = options.listen;
  47. if (typeof listen === "object" && !("port" in listen))
  48. listen = { ...listen, port: undefined };
  49. server.listen(listen);
  50. };
  51. const protocol = options.protocol || (isHttps ? "https" : "http");
  52. /** @type {RequestListener} */
  53. const requestListener = (req, res) => {
  54. if (req.url === undefined) return;
  55. const keys = req.url.slice(prefix.length).split("@");
  56. req.socket.on("close", () => {
  57. setTimeout(() => {
  58. for (const key of keys) {
  59. const oldValue = activeModules.get(key) || 0;
  60. activeModules.set(key, oldValue - 1);
  61. if (oldValue === 1) {
  62. logger.log(
  63. `${key} is no longer in use. Next compilation will skip this module.`
  64. );
  65. }
  66. }
  67. }, 120000);
  68. });
  69. req.socket.setNoDelay(true);
  70. res.writeHead(200, {
  71. "content-type": "text/event-stream",
  72. "Access-Control-Allow-Origin": "*",
  73. "Access-Control-Allow-Methods": "*",
  74. "Access-Control-Allow-Headers": "*"
  75. });
  76. res.write("\n");
  77. let moduleActivated = false;
  78. for (const key of keys) {
  79. const oldValue = activeModules.get(key) || 0;
  80. activeModules.set(key, oldValue + 1);
  81. if (oldValue === 0) {
  82. logger.log(`${key} is now in use and will be compiled.`);
  83. moduleActivated = true;
  84. }
  85. }
  86. if (moduleActivated && compiler.watching) compiler.watching.invalidate();
  87. };
  88. const server = /** @type {Server} */ (createServer());
  89. server.on("request", requestListener);
  90. let isClosing = false;
  91. /** @type {Set<import("net").Socket>} */
  92. const sockets = new Set();
  93. server.on("connection", socket => {
  94. sockets.add(socket);
  95. socket.on("close", () => {
  96. sockets.delete(socket);
  97. });
  98. if (isClosing) socket.destroy();
  99. });
  100. server.on("clientError", e => {
  101. if (e.message !== "Server is disposing") logger.warn(e);
  102. });
  103. server.on(
  104. "listening",
  105. /**
  106. * @param {Error} err error
  107. * @returns {void}
  108. */
  109. err => {
  110. if (err) return callback(err);
  111. const _addr = server.address();
  112. if (typeof _addr === "string")
  113. throw new Error("addr must not be a string");
  114. const addr = /** @type {AddressInfo} */ (_addr);
  115. const urlBase =
  116. addr.address === "::" || addr.address === "0.0.0.0"
  117. ? `${protocol}://localhost:${addr.port}`
  118. : addr.family === "IPv6"
  119. ? `${protocol}://[${addr.address}]:${addr.port}`
  120. : `${protocol}://${addr.address}:${addr.port}`;
  121. logger.log(
  122. `Server-Sent-Events server for lazy compilation open at ${urlBase}.`
  123. );
  124. callback(null, {
  125. dispose(callback) {
  126. isClosing = true;
  127. // Removing the listener is a workaround for a memory leak in node.js
  128. server.off("request", requestListener);
  129. server.close(err => {
  130. callback(err);
  131. });
  132. for (const socket of sockets) {
  133. socket.destroy(new Error("Server is disposing"));
  134. }
  135. },
  136. module(originalModule) {
  137. const key = `${encodeURIComponent(
  138. originalModule.identifier().replace(/\\/g, "/").replace(/@/g, "_")
  139. ).replace(/%(2F|3A|24|26|2B|2C|3B|3D)/g, decodeURIComponent)}`;
  140. const active = activeModules.get(key) > 0;
  141. return {
  142. client: `${options.client}?${encodeURIComponent(urlBase + prefix)}`,
  143. data: key,
  144. active
  145. };
  146. }
  147. });
  148. }
  149. );
  150. listen(server);
  151. };