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