lazyCompilationBackend.js 4.4 KB

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