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.
194 lines (182 loc) • 6.24 kB
JavaScript
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
;
/** @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);
});
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);
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)/g, decodeURIComponent)}`;
const active = /** @type {number} */ (activeModules.get(key)) > 0;
return {
client: `${options.client}?${encodeURIComponent(urlBase + prefix)}`,
data: key,
active
};
}
});
}
);
listen(server);
};