144 lines
4.4 KiB
JavaScript
144 lines
4.4 KiB
JavaScript
|
/*
|
||
|
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 */
|
||
|
/** @typedef {import("../../declarations/WebpackOptions").LazyCompilationDefaultBackendOptions} LazyCompilationDefaultBackendOptions */
|
||
|
/** @typedef {import("../Compiler")} Compiler */
|
||
|
|
||
|
/**
|
||
|
* @callback BackendHandler
|
||
|
* @param {Compiler} compiler compiler
|
||
|
* @param {function((Error | null)=, any=): void} callback callback
|
||
|
* @returns {void}
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @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");
|
||
|
const activeModules = new Map();
|
||
|
const prefix = "/lazy-compilation-using-";
|
||
|
|
||
|
const isHttps =
|
||
|
options.protocol === "https" ||
|
||
|
(typeof options.server === "object" &&
|
||
|
("key" in options.server || "pfx" in options.server));
|
||
|
|
||
|
const createServer =
|
||
|
typeof options.server === "function"
|
||
|
? options.server
|
||
|
: (() => {
|
||
|
const http = isHttps ? require("https") : require("http");
|
||
|
return http.createServer.bind(http, options.server);
|
||
|
})();
|
||
|
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");
|
||
|
|
||
|
const requestListener = (req, res) => {
|
||
|
const keys = req.url.slice(prefix.length).split("@");
|
||
|
req.socket.on("close", () => {
|
||
|
setTimeout(() => {
|
||
|
for (const key of keys) {
|
||
|
const oldValue = activeModules.get(key) || 0;
|
||
|
activeModules.set(key, oldValue - 1);
|
||
|
if (oldValue === 1) {
|
||
|
logger.log(
|
||
|
`${key} is no longer in use. Next compilation will skip this module.`
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}, 120000);
|
||
|
});
|
||
|
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 = /** @type {import("net").Server} */ (createServer());
|
||
|
server.on("request", requestListener);
|
||
|
|
||
|
let isClosing = false;
|
||
|
/** @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 => {
|
||
|
if (e.message !== "Server is disposing") logger.warn(e);
|
||
|
});
|
||
|
server.on("listening", err => {
|
||
|
if (err) return callback(err);
|
||
|
const addr = server.address();
|
||
|
if (typeof addr === "string") throw new Error("addr must not be a string");
|
||
|
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;
|
||
|
// Removing the listener is a workaround for a memory leak in node.js
|
||
|
server.off("request", requestListener);
|
||
|
server.close(err => {
|
||
|
callback(err);
|
||
|
});
|
||
|
for (const socket of sockets) {
|
||
|
socket.destroy(new Error("Server is disposing"));
|
||
|
}
|
||
|
},
|
||
|
module(originalModule) {
|
||
|
const key = `${encodeURIComponent(
|
||
|
originalModule.identifier().replace(/\\/g, "/").replace(/@/g, "_")
|
||
|
).replace(/%(2F|3A|24|26|2B|2C|3B|3D|3A)/g, decodeURIComponent)}`;
|
||
|
const active = activeModules.get(key) > 0;
|
||
|
return {
|
||
|
client: `${options.client}?${encodeURIComponent(urlBase + prefix)}`,
|
||
|
data: key,
|
||
|
active
|
||
|
};
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
listen(server);
|
||
|
};
|