UNPKG

webpack

Version:

Packs ECMAScript/CommonJs/AMD modules for the browser. Allows you to split your codebase into multiple bundles, which can be loaded on demand. Supports loaders to preprocess files, i.e. json, jsx, es7, css, less, ... and your custom stuff.

209 lines (197 loc) 7.04 kB
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; /** @typedef {import("http").RequestListener} RequestListener */ /** @typedef {import("http").ServerOptions} HttpServerOptions */ /** @typedef {import("http").Server} HttpServer */ /** @typedef {import("https").ServerOptions} HttpsServerOptions */ /** @typedef {import("https").Server} HttpsServer */ /** @typedef {import("net").AddressInfo} AddressInfo */ /** @typedef {import("./LazyCompilationPlugin").BackendHandler} BackendHandler */ /** @typedef {import("../../declarations/WebpackOptions").LazyCompilationDefaultBackendOptions} LazyCompilationDefaultBackendOptions */ /** @typedef {HttpServer | HttpsServer} Server */ /** @typedef {(server: Server) => void} Listen */ /** @typedef {() => Server} CreateServerFunction */ /** * Returns backend. * @param {Omit<LazyCompilationDefaultBackendOptions, "client"> & { client: NonNullable<LazyCompilationDefaultBackendOptions["client"]> }} options additional options for the backend * @returns {BackendHandler} backend */ module.exports = (options) => (compiler, callback) => { const logger = compiler.getInfrastructureLogger("LazyCompilationBackend"); /** @type {Map<string, number>} */ const activeModules = new Map(); /** @type {Set<NodeJS.Timeout>} */ const idleTimers = new Set(); let isClosing = false; const prefix = "/lazy-compilation-using-"; const isHttps = options.protocol === "https" || (typeof options.server === "object" && ("key" in options.server || "pfx" in options.server)); /** @type {CreateServerFunction} */ const createServer = typeof options.server === "function" ? options.server : (() => { const http = isHttps ? require("https") : require("http"); return /** @type {(this: import("http") | import("https"), options: HttpServerOptions | HttpsServerOptions) => Server} */ ( http.createServer ).bind( http, /** @type {HttpServerOptions | HttpsServerOptions} */ (options.server) ); })(); /** @type {Listen} */ const listen = typeof options.listen === "function" ? options.listen : (server) => { let listen = options.listen; if (typeof listen === "object" && !("port" in listen)) { listen = { ...listen, port: undefined }; } server.listen(listen); }; const protocol = options.protocol || (isHttps ? "https" : "http"); /** @type {RequestListener} */ const requestListener = (req, res) => { if (req.url === undefined) return; const keys = req.url.slice(prefix.length).split("@"); req.socket.on("close", () => { // Shutting down: skip the idle-decrement timer entirely. if (isClosing) return; const timer = setTimeout(() => { idleTimers.delete(timer); for (const key of keys) { const oldValue = activeModules.get(key) || 0; // Drop the entry at zero so the map doesn't retain idle modules // and the count can't drift negative. if (oldValue <= 1) { activeModules.delete(key); if (oldValue === 1) { logger.log( `${key} is no longer in use. Next compilation will skip this module.` ); } } else { activeModules.set(key, oldValue - 1); } } }, 120000); // Don't keep the process alive just to decrement idle counters. if (timer.unref) timer.unref(); idleTimers.add(timer); }); // Not all runtimes expose `setNoDelay` on the request socket (e.g. Deno's // HTTP server); it's only a latency optimization, so skip it when absent. if (req.socket.setNoDelay) req.socket.setNoDelay(true); res.writeHead(200, { "content-type": "text/event-stream", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "*", "Access-Control-Allow-Headers": "*" }); res.write("\n"); let moduleActivated = false; for (const key of keys) { const oldValue = activeModules.get(key) || 0; activeModules.set(key, oldValue + 1); if (oldValue === 0) { logger.log(`${key} is now in use and will be compiled.`); moduleActivated = true; } } if (moduleActivated && compiler.watching) compiler.watching.invalidate(); }; const server = createServer(); server.on("request", requestListener); /** @type {Set<import("net").Socket>} */ const sockets = new Set(); server.on("connection", (socket) => { sockets.add(socket); socket.on("close", () => { sockets.delete(socket); }); if (isClosing) socket.destroy(); }); server.on("clientError", (e) => { // A closing browser tab or a test tearing down aborts the request and // resets the socket; that surfaces here as ECONNRESET and is benign. if ( e.message === "Server is disposing" || /** @type {NodeJS.ErrnoException} */ (e).code === "ECONNRESET" ) { return; } logger.warn(e); }); server.on( "listening", /** * Handles the callback logic for this hook. * @param {Error} err error * @returns {void} */ (err) => { if (err) return callback(err); const _addr = server.address(); if (typeof _addr === "string") { throw new Error("addr must not be a string"); } const addr = /** @type {AddressInfo} */ (_addr); const urlBase = addr.address === "::" || addr.address === "0.0.0.0" ? `${protocol}://localhost:${addr.port}` : addr.family === "IPv6" ? `${protocol}://[${addr.address}]:${addr.port}` : `${protocol}://${addr.address}:${addr.port}`; logger.log( `Server-Sent-Events server for lazy compilation open at ${urlBase}.` ); callback(null, { dispose(callback) { isClosing = true; for (const timer of idleTimers) clearTimeout(timer); idleTimers.clear(); // Removing the listener is a workaround for a memory leak in node.js server.off("request", requestListener); for (const socket of sockets) { socket.destroy(new Error("Server is disposing")); } // Some runtimes (e.g. Deno) don't emit "connection", so `sockets` // misses the open SSE connections and `server.close` would hang; // force-close everything before waiting for it. if (server.closeAllConnections) server.closeAllConnections(); server.close((err) => { // `closeAllConnections()` already stops the server on some runtimes // (e.g. Bun), so `close` then reports ERR_SERVER_NOT_RUNNING; the // server is closed either way, so that's not a dispose failure. callback( err && /** @type {NodeJS.ErrnoException} */ (err).code !== "ERR_SERVER_NOT_RUNNING" ? err : null ); }); }, module(originalModule) { const key = `${encodeURIComponent( originalModule.identifier().replace(/\\/g, "/").replace(/@/g, "_") ).replace(/%(2F|3A|24|26|2B|2C|3B|3D)/g, decodeURIComponent)}`; const active = /** @type {number} */ (activeModules.get(key)) > 0; return { client: `${options.client}?${encodeURIComponent(urlBase + prefix)}`, data: key, active }; } }); } ); listen(server); };