lazyCompilationBackend.js 5.3 KB

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