lazyCompilationBackend.js 5.2 KB

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