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