| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211 |
- /*
- MIT License http://www.opensource.org/licenses/mit-license.php
- Author Tobias Koppers @sokra
- */
- "use strict";
- /** @typedef {import("http").ServerOptions} HttpServerOptions */
- /** @typedef {import("https").ServerOptions} HttpsServerOptions */
- /** @import { RequestListener, Server as HttpServer } from "http" */
- /** @import { Server as HttpsServer } from "https" */
- /** @import { AddressInfo } from "net" */
- /** @import { BackendHandler } from "./LazyCompilationPlugin" */
- /**
- * @import {
- * LazyCompilationDefaultBackendOptions
- * } from "../../declarations/WebpackOptions"
- */
- /** @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);
- };
|