From 51af84e52dfb482419c866317b9c048cfbe27176 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Thu, 15 Aug 2024 22:05:21 +0300 Subject: [PATCH 01/11] feat: http2 support for `connect` compatibility application --- lib/Server.js | 33 ++++++++++++++++++++------------- lib/options.json | 4 ++-- test/e2e/app.test.js | 11 ++++++++--- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/lib/Server.js b/lib/Server.js index 1a7b572d9b..ab5b94cb44 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -101,9 +101,13 @@ const schema = require("./options.json"); * @property {false | WatchOptions} watch */ +/** + * @typedef {"http" | "https" | "spdy" | "http2" | string} ServerType + */ + /** * @typedef {Object} ServerConfiguration - * @property {"http" | "https" | "spdy" | string} [type] + * @property {ServerType} [type] * @property {ServerOptions} [options] */ @@ -210,8 +214,7 @@ const schema = require("./options.json"); * @property {boolean | Record | BonjourOptions} [bonjour] * @property {string | string[] | WatchFiles | Array} [watchFiles] * @property {boolean | string | Static | Array} [static] - * @property {boolean | ServerOptions} [https] - * @property {"http" | "https" | "spdy" | string | ServerConfiguration} [server] + * @property {ServerType | ServerConfiguration} [server] * @property {() => Promise} [app] * @property {boolean | "sockjs" | "ws" | string | WebSocketServerConfiguration} [webSocketServer] * @property {ProxyConfigArray} [proxy] @@ -1096,7 +1099,6 @@ class Server { ? /** @type {ServerConfiguration} */ (options.server).type || "http" : "http", options: { - .../** @type {ServerOptions} */ (options.https), .../** @type {ServerConfiguration} */ (options.server || {}).options, }, }; @@ -1112,7 +1114,11 @@ class Server { }; } - if (options.server.type === "https" || options.server.type === "spdy") { + if ( + options.server.type === "https" || + options.server.type === "http2" || + options.server.type === "spdy" + ) { if ( typeof ( /** @type {ServerOptions} */ (options.server.options).requestCert @@ -2438,16 +2444,17 @@ class Server { * @returns {void} */ createServer() { - const { type, options } = /** @type {ServerConfiguration} */ ( - this.options.server - ); + const { type, options } = + /** @type {ServerConfiguration} */ + (this.options.server); - /** @type {import("http").Server | undefined | null} */ // eslint-disable-next-line import/no-dynamic-require - this.server = require(/** @type {string} */ (type)).createServer( - options, - this.app, - ); + const serverType = require(/** @type {string} */ (type)); + /** @type {import("http").Server | import("http2").Http2SecureServer | undefined | null} */ + this.server = + type === "http2" + ? serverType.createSecureServer(options, this.app) + : serverType.createServer(options, this.app); /** @type {import("http").Server} */ (this.server).on( diff --git a/lib/options.json b/lib/options.json index 50e29376df..019731e26d 100644 --- a/lib/options.json +++ b/lib/options.json @@ -526,10 +526,10 @@ "description": "Allows to set server and options (by default 'http')." }, "ServerType": { - "enum": ["http", "https", "spdy"] + "enum": ["http", "https", "spdy", "http2"] }, "ServerEnum": { - "enum": ["http", "https", "spdy"], + "enum": ["http", "https", "spdy", "http2"], "cli": { "exclude": true } diff --git a/test/e2e/app.test.js b/test/e2e/app.test.js index 05e6c23a79..959be538f7 100644 --- a/test/e2e/app.test.js +++ b/test/e2e/app.test.js @@ -13,12 +13,17 @@ const staticDirectory = path.resolve( ); const apps = [ - ["express", () => require("express")()], + // ["express", () => require("express")()], ["connect", () => require("connect")()], - ["connect (async)", async () => require("express")()], + // ["connect (async)", async () => require("express")()], ]; -const servers = ["http", "https", "spdy"]; +const servers = [ + // "http", + // "https", + // "spdy", + "http2", +]; describe("app option", () => { for (const [appName, app] of apps) { From 730c011a9245251aebe6dbd55de141171dffc556 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Thu, 15 Aug 2024 22:17:11 +0300 Subject: [PATCH 02/11] refactor: rebase --- types/lib/Server.d.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/types/lib/Server.d.ts b/types/lib/Server.d.ts index 0f0ed815d0..cbfd503434 100644 --- a/types/lib/Server.d.ts +++ b/types/lib/Server.d.ts @@ -1302,8 +1302,12 @@ declare class Server< * @returns {void} */ private createServer; - /** @type {import("http").Server | undefined | null} */ - server: import("http").Server | undefined | null; + /** @type {import("http").Server | import("http2").Http2SecureServer | undefined | null} */ + server: + | import("http").Server + | import("http2").Http2SecureServer + | undefined + | null; /** * @private * @returns {void} @@ -1449,6 +1453,7 @@ declare namespace Server { WatchFiles, Static, NormalizedStatic, + ServerType, ServerConfiguration, WebSocketServerConfiguration, ClientConnection, @@ -1579,6 +1584,7 @@ type NormalizedStatic = { staticOptions: ServeStaticOptions; watch: false | WatchOptions; }; +type ServerType = "http" | "https" | "spdy" | "http2" | string; type ServerConfiguration = { type?: string | undefined; options?: ServerOptions | undefined; @@ -1716,7 +1722,6 @@ type Configuration = | (string | WatchFiles)[] | undefined; static?: string | boolean | Static | (string | Static)[] | undefined; - https?: boolean | ServerOptions | undefined; server?: string | ServerConfiguration | undefined; app?: (() => Promise) | undefined; webSocketServer?: From ff7f35ae0036c29749b615b6605a81d039a8d981 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Fri, 16 Aug 2024 00:39:32 +0300 Subject: [PATCH 03/11] test: fix --- test/__snapshots__/validate-options.test.js.snap.webpack5 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/__snapshots__/validate-options.test.js.snap.webpack5 b/test/__snapshots__/validate-options.test.js.snap.webpack5 index 098192f990..6ce65c5be2 100644 --- a/test/__snapshots__/validate-options.test.js.snap.webpack5 +++ b/test/__snapshots__/validate-options.test.js.snap.webpack5 @@ -533,7 +533,7 @@ exports[`options validate should throw an error on the "server" option with '{"t exports[`options validate should throw an error on the "server" option with '{"type":"https","options":{"ca":true}}' value 1`] = ` "ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema. - options.server should be one of these: - "http" | "https" | "spdy" | non-empty string | object { type?, options? } + "http" | "https" | "spdy" | "http2" | non-empty string | object { type?, options? } -> Allows to set server and options (by default 'http'). -> Read more at https://webpack.js.org/configuration/dev-server/#devserverserver Details: @@ -550,7 +550,7 @@ exports[`options validate should throw an error on the "server" option with '{"t exports[`options validate should throw an error on the "server" option with '{"type":"https","options":{"cert":true}}' value 1`] = ` "ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema. - options.server should be one of these: - "http" | "https" | "spdy" | non-empty string | object { type?, options? } + "http" | "https" | "spdy" | "http2" | non-empty string | object { type?, options? } -> Allows to set server and options (by default 'http'). -> Read more at https://webpack.js.org/configuration/dev-server/#devserverserver Details: @@ -567,7 +567,7 @@ exports[`options validate should throw an error on the "server" option with '{"t exports[`options validate should throw an error on the "server" option with '{"type":"https","options":{"key":10}}' value 1`] = ` "ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema. - options.server should be one of these: - "http" | "https" | "spdy" | non-empty string | object { type?, options? } + "http" | "https" | "spdy" | "http2" | non-empty string | object { type?, options? } -> Allows to set server and options (by default 'http'). -> Read more at https://webpack.js.org/configuration/dev-server/#devserverserver Details: @@ -590,7 +590,7 @@ exports[`options validate should throw an error on the "server" option with '{"t exports[`options validate should throw an error on the "server" option with '{"type":"https","options":{"pfx":10}}' value 1`] = ` "ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema. - options.server should be one of these: - "http" | "https" | "spdy" | non-empty string | object { type?, options? } + "http" | "https" | "spdy" | "http2" | non-empty string | object { type?, options? } -> Allows to set server and options (by default 'http'). -> Read more at https://webpack.js.org/configuration/dev-server/#devserverserver Details: From 2894c65a04ae220f870399c60b69f33ebc49a2ed Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Fri, 16 Aug 2024 02:16:09 +0300 Subject: [PATCH 04/11] refactor: fix logic --- lib/Server.js | 75 +++++++++++-------- .../__snapshots__/app.test.js.snap.webpack5 | 56 ++++++++++++++ test/e2e/app.test.js | 20 +++-- types/lib/Server.d.ts | 13 ++-- 4 files changed, 114 insertions(+), 50 deletions(-) diff --git a/lib/Server.js b/lib/Server.js index ab5b94cb44..a246daace7 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -200,7 +200,7 @@ const schema = require("./options.json"); */ /** - * @template {BasicApplication} [T=ExpressApplication] + * @template {BasicApplication} [A=ExpressApplication] * @typedef {Object} Configuration * @property {boolean | string} [ipc] * @property {Host} [host] @@ -215,15 +215,15 @@ const schema = require("./options.json"); * @property {string | string[] | WatchFiles | Array} [watchFiles] * @property {boolean | string | Static | Array} [static] * @property {ServerType | ServerConfiguration} [server] - * @property {() => Promise} [app] + * @property {() => Promise} [app] * @property {boolean | "sockjs" | "ws" | string | WebSocketServerConfiguration} [webSocketServer] * @property {ProxyConfigArray} [proxy] * @property {boolean | string | Open | Array} [open] * @property {boolean} [setupExitSignals] * @property {boolean | ClientConfiguration} [client] * @property {Headers | ((req: Request, res: Response, context: DevMiddlewareContext) => Headers)} [headers] - * @property {(devServer: Server) => void} [onListening] - * @property {(middlewares: Middleware[], devServer: Server) => Middleware[]} [setupMiddlewares] + * @property {(devServer: Server) => void} [onListening] + * @property {(middlewares: Middleware[], devServer: Server) => Middleware[]} [setupMiddlewares] */ if (!process.env.WEBPACK_SERVE) { @@ -301,11 +301,12 @@ function useFn(route, fn) { */ /** - * @template {BasicApplication} [T=ExpressApplication] + * @template {BasicApplication} [A=ExpressApplication] + * @template {import("http").Server} [S=import("http").Server] */ class Server { /** - * @param {Configuration} options + * @param {Configuration} options * @param {Compiler | MultiCompiler} compiler */ constructor(options = {}, compiler) { @@ -1804,7 +1805,7 @@ class Server { // https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade /** @type {RequestHandler[]} */ (this.webSocketProxies).forEach((webSocketProxy) => { - /** @type {import("http").Server} */ + /** @type {S} */ (this.server).on( "upgrade", /** @type {RequestHandler & { upgrade: NonNullable }} */ @@ -1818,7 +1819,7 @@ class Server { * @returns {Promise} */ async setupApp() { - /** @type {T | undefined}*/ + /** @type {A | undefined}*/ this.app = typeof this.options.app === "function" ? await this.options.app() @@ -1877,13 +1878,14 @@ class Server { * @returns {void} */ setupHostHeaderCheck() { - /** @type {T} */ + /** @type {A} */ (this.app).use((req, res, next) => { + const headerName = req.headers[":authority"] ? ":authority" : "host"; if ( this.checkHeader( /** @type {{ [key: string]: string | undefined }} */ (req.headers), - "host", + headerName, ) ) { next(); @@ -1916,7 +1918,7 @@ class Server { setupBuiltInRoutes() { const { app, middleware } = this; - /** @type {T} */ + /** @type {A} */ (app).use("/__webpack_dev_server__/sockjs.bundle.js", (req, res, next) => { if (req.method !== "GET" && req.method !== "HEAD") { next(); @@ -1958,7 +1960,7 @@ class Server { fs.createReadStream(clientPath).pipe(res); }); - /** @type {T} */ + /** @type {A} */ (app).use("/webpack-dev-server/invalidate", (req, res, next) => { if (req.method !== "GET" && req.method !== "HEAD") { next(); @@ -1970,7 +1972,7 @@ class Server { res.end(); }); - /** @type {T} */ + /** @type {A} */ (app).use("/webpack-dev-server/open-editor", (req, res, next) => { if (req.method !== "GET" && req.method !== "HEAD") { next(); @@ -1996,7 +1998,7 @@ class Server { res.end(); }); - /** @type {T} */ + /** @type {A} */ (app).use("/webpack-dev-server", (req, res, next) => { if (req.method !== "GET" && req.method !== "HEAD") { next(); @@ -2070,9 +2072,10 @@ class Server { * @returns {void} */ setupWatchStaticFiles() { - if (/** @type {NormalizedStatic[]} */ (this.options.static).length > 0) { - /** @type {NormalizedStatic[]} */ - (this.options.static).forEach((staticOption) => { + const watchFiles = /** @type {NormalizedStatic[]} */ (this.options.static); + + if (watchFiles.length > 0) { + watchFiles.forEach((staticOption) => { if (staticOption.watch) { this.watchFiles(staticOption.directory, staticOption.watch); } @@ -2085,11 +2088,10 @@ class Server { * @returns {void} */ setupWatchFiles() { - const { watchFiles } = this.options; + const watchFiles = /** @type {WatchFiles[]} */ (this.options.watchFiles); - if (/** @type {WatchFiles[]} */ (watchFiles).length > 0) { - /** @type {WatchFiles[]} */ - (watchFiles).forEach((item) => { + if (watchFiles.length > 0) { + watchFiles.forEach((item) => { this.watchFiles(item.paths, item.options); }); } @@ -2106,7 +2108,11 @@ class Server { let middlewares = []; // compress is placed last and uses unshift so that it will be the first middleware used - if (this.options.compress) { + if ( + this.options.compress && + /** @type {ServerConfiguration} */ + (this.options.server).type !== "http2" + ) { const compression = require("compression"); middlewares.push({ name: "compression", middleware: compression() }); @@ -2417,20 +2423,20 @@ class Server { middlewares.forEach((middleware) => { if (typeof middleware === "function") { - /** @type {T} */ + /** @type {A} */ (this.app).use( /** @type {NextHandleFunction | HandleFunction} */ (middleware), ); } else if (typeof middleware.path !== "undefined") { - /** @type {T} */ + /** @type {A} */ (this.app).use( middleware.path, /** @type {SimpleHandleFunction | NextHandleFunction} */ (middleware.middleware), ); } else { - /** @type {T} */ + /** @type {A} */ (this.app).use( /** @type {NextHandleFunction | HandleFunction} */ (middleware.middleware), @@ -2450,13 +2456,16 @@ class Server { // eslint-disable-next-line import/no-dynamic-require const serverType = require(/** @type {string} */ (type)); - /** @type {import("http").Server | import("http2").Http2SecureServer | undefined | null} */ + /** @type {S | null | undefined}*/ this.server = type === "http2" - ? serverType.createSecureServer(options, this.app) + ? serverType.createSecureServer( + { ...options, allowHTTP1: true }, + this.app, + ) : serverType.createServer(options, this.app); - /** @type {import("http").Server} */ + /** @type {S} */ (this.server).on( "connection", /** @@ -2473,7 +2482,7 @@ class Server { }, ); - /** @type {import("http").Server} */ + /** @type {S} */ (this.server).on( "error", /** @@ -2758,7 +2767,7 @@ class Server { if (this.options.ipc) { this.logger.info( `Project is running at: "${ - /** @type {import("http").Server} */ + /** @type {S} */ (this.server).address() }"`, ); @@ -2769,7 +2778,7 @@ class Server { const { address, port } = /** @type {import("net").AddressInfo} */ ( - /** @type {import("http").Server} */ + /** @type {S} */ (this.server).address() ); /** @@ -3244,7 +3253,7 @@ class Server { await /** @type {Promise} */ ( new Promise((resolve) => { - /** @type {import("http").Server} */ + /** @type {S} */ (this.server).listen(listenOptions, () => { resolve(); }); @@ -3330,7 +3339,7 @@ class Server { if (this.server) { await /** @type {Promise} */ ( new Promise((resolve) => { - /** @type {import("http").Server} */ + /** @type {S} */ (this.server).close(() => { this.server = null; diff --git a/test/e2e/__snapshots__/app.test.js.snap.webpack5 b/test/e2e/__snapshots__/app.test.js.snap.webpack5 index 9ac3495c9b..7569392a9d 100644 --- a/test/e2e/__snapshots__/app.test.js.snap.webpack5 +++ b/test/e2e/__snapshots__/app.test.js.snap.webpack5 @@ -28,6 +28,34 @@ exports[`app option should work using "connect (async)" application and "http" s " `; +exports[`app option should work using "connect (async)" application and "http2" server should handle GET request to index route (/): console messages 1`] = ` +[ + "[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.", + "[HMR] Waiting for update signal from WDS...", + "Hey.", +] +`; + +exports[`app option should work using "connect (async)" application and "http2" server should handle GET request to index route (/): page errors 1`] = `[]`; + +exports[`app option should work using "connect (async)" application and "http2" server should handle GET request to index route (/): response status 1`] = `200`; + +exports[`app option should work using "connect (async)" application and "http2" server should handle GET request to index route (/): response text 1`] = ` +" + + + + + webpack-dev-server + + +

webpack-dev-server is running...

+ + + +" +`; + exports[`app option should work using "connect (async)" application and "https" server should handle GET request to index route (/): console messages 1`] = ` [ "[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.", @@ -112,6 +140,34 @@ exports[`app option should work using "connect" application and "http" server sh " `; +exports[`app option should work using "connect" application and "http2" server should handle GET request to index route (/): console messages 1`] = ` +[ + "[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.", + "[HMR] Waiting for update signal from WDS...", + "Hey.", +] +`; + +exports[`app option should work using "connect" application and "http2" server should handle GET request to index route (/): page errors 1`] = `[]`; + +exports[`app option should work using "connect" application and "http2" server should handle GET request to index route (/): response status 1`] = `200`; + +exports[`app option should work using "connect" application and "http2" server should handle GET request to index route (/): response text 1`] = ` +" + + + + + webpack-dev-server + + +

webpack-dev-server is running...

+ + + +" +`; + exports[`app option should work using "connect" application and "https" server should handle GET request to index route (/): console messages 1`] = ` [ "[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.", diff --git a/test/e2e/app.test.js b/test/e2e/app.test.js index 959be538f7..c7a73f4f77 100644 --- a/test/e2e/app.test.js +++ b/test/e2e/app.test.js @@ -13,21 +13,21 @@ const staticDirectory = path.resolve( ); const apps = [ - // ["express", () => require("express")()], + ["express", () => require("express")()], ["connect", () => require("connect")()], - // ["connect (async)", async () => require("express")()], + ["connect (async)", () => require("connect")()], ]; -const servers = [ - // "http", - // "https", - // "spdy", - "http2", -]; +const servers = ["http", "https", "spdy", "http2"]; describe("app option", () => { for (const [appName, app] of apps) { for (const server of servers) { + if (appName === "express" && server === "http2") { + // eslint-disable-next-line no-continue + continue; + } + let compiler; let devServer; let page; @@ -87,9 +87,7 @@ describe("app option", () => { () => performance.getEntries()[0].nextHopProtocol, ); - const isSpdy = server === "spdy"; - - if (isSpdy) { + if (server === "spdy" || server === "http2") { expect(HTTPVersion).toEqual("h2"); } else { expect(HTTPVersion).toEqual("http/1.1"); diff --git a/types/lib/Server.d.ts b/types/lib/Server.d.ts index cbfd503434..6b4498ee1d 100644 --- a/types/lib/Server.d.ts +++ b/types/lib/Server.d.ts @@ -5,9 +5,14 @@ export = Server; */ /** * @template {BasicApplication} [T=ExpressApplication] + * @template {import("http").Server} [S=import("http").Server] */ declare class Server< T extends BasicApplication = import("express").Application, + S extends import("http").Server = import("http").Server< + typeof import("http").IncomingMessage, + typeof import("http").ServerResponse + >, > { static get schema(): { title: string; @@ -1302,12 +1307,8 @@ declare class Server< * @returns {void} */ private createServer; - /** @type {import("http").Server | import("http2").Http2SecureServer | undefined | null} */ - server: - | import("http").Server - | import("http2").Http2SecureServer - | undefined - | null; + /** @type {S | null | undefined}*/ + server: S | null | undefined; /** * @private * @returns {void} From 303aa4985144feba31f10a00edfc2c483f2cbab2 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Fri, 16 Aug 2024 02:23:01 +0300 Subject: [PATCH 05/11] refactor: udate tyes --- types/lib/Server.d.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/types/lib/Server.d.ts b/types/lib/Server.d.ts index 6b4498ee1d..e52b2e5276 100644 --- a/types/lib/Server.d.ts +++ b/types/lib/Server.d.ts @@ -4,11 +4,11 @@ export = Server; * @property {typeof useFn} use */ /** - * @template {BasicApplication} [T=ExpressApplication] + * @template {BasicApplication} [A=ExpressApplication] * @template {import("http").Server} [S=import("http").Server] */ declare class Server< - T extends BasicApplication = import("express").Application, + A extends BasicApplication = import("express").Application, S extends import("http").Server = import("http").Server< typeof import("http").IncomingMessage, typeof import("http").ServerResponse @@ -1165,11 +1165,11 @@ declare class Server< */ private static isWebTarget; /** - * @param {Configuration} options + * @param {Configuration
} options * @param {Compiler | MultiCompiler} compiler */ constructor( - options: Configuration | undefined, + options: Configuration | undefined, compiler: Compiler | MultiCompiler, ); compiler: import("webpack").Compiler | import("webpack").MultiCompiler; @@ -1177,7 +1177,7 @@ declare class Server< * @type {ReturnType} * */ logger: ReturnType; - options: Configuration; + options: Configuration; /** * @type {FSWatcher[]} */ @@ -1241,8 +1241,8 @@ declare class Server< * @returns {Promise} */ private setupApp; - /** @type {T | undefined}*/ - app: T | undefined; + /** @type {A | undefined}*/ + app: A | undefined; /** * @private * @param {Stats | MultiStats} statsObj @@ -1686,7 +1686,7 @@ type Middleware = middleware: MiddlewareHandler; } | MiddlewareHandler; -type Configuration = +type Configuration = { ipc?: string | boolean | undefined; host?: string | undefined; @@ -1724,7 +1724,7 @@ type Configuration = | undefined; static?: string | boolean | Static | (string | Static)[] | undefined; server?: string | ServerConfiguration | undefined; - app?: (() => Promise) | undefined; + app?: (() => Promise) | undefined; webSocketServer?: | string | boolean @@ -1742,9 +1742,9 @@ type Configuration = context: DevMiddlewareContext, ) => Headers) | undefined; - onListening?: ((devServer: Server) => void) | undefined; + onListening?: ((devServer: Server) => void) | undefined; setupMiddlewares?: - | ((middlewares: Middleware[], devServer: Server) => Middleware[]) + | ((middlewares: Middleware[], devServer: Server) => Middleware[]) | undefined; }; type BasicApplication = { From 5c164c03414cc5b01cab3a6d481a3cb75fd325cd Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Fri, 16 Aug 2024 15:00:14 +0300 Subject: [PATCH 06/11] refactor: code --- lib/Server.js | 117 +++++++++--------- .../watch-files-config/webpack.config.js | 1 + 2 files changed, 62 insertions(+), 56 deletions(-) diff --git a/lib/Server.js b/lib/Server.js index a246daace7..21bfda572c 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -391,7 +391,7 @@ class Server { if (gatewayOrFamily === "v4" || gatewayOrFamily === "v6") { let host; - Object.values(os.networkInterfaces()) + const networks = Object.values(os.networkInterfaces()) .flatMap((networks) => networks ?? []) .filter((network) => { if (!network || !network.address) { @@ -415,14 +415,16 @@ class Server { } return network.address; - }) - .forEach((network) => { - host = network.address; - if (host.includes(":")) { - host = `[${host}]`; - } }); + for (const network of networks) { + host = network.address; + + if (host.includes(":")) { + host = `[${host}]`; + } + } + return host; } @@ -1366,7 +1368,7 @@ class Server { */ const result = []; - options.open.forEach((item) => { + for (const item of options.open) { if (typeof item === "string") { result.push({ target: item, options: defaultOpenOptions }); @@ -1374,7 +1376,7 @@ class Server { } result.push(...getOpenItemsFromObject(item)); - }); + } /** @type {NormalizedOpen[]} */ (options.open) = result; @@ -1721,7 +1723,7 @@ class Server { /** @type {MultiCompiler} */ (this.compiler).compilers || [this.compiler]; - compilers.forEach((compiler) => { + for (const compiler of compilers) { this.addAdditionalEntries(compiler); const webpack = compiler.webpack || require("webpack"); @@ -1746,7 +1748,7 @@ class Server { plugin.apply(compiler); } } - }); + } if ( this.options.client && @@ -1803,15 +1805,18 @@ class Server { // Proxy WebSocket without the initial http request // https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade - /** @type {RequestHandler[]} */ - (this.webSocketProxies).forEach((webSocketProxy) => { + const webSocketProxies = + /** @type {RequestHandler[]} */ + (this.webSocketProxies); + + for (const webSocketProxy of webSocketProxies) { /** @type {S} */ (this.server).on( "upgrade", /** @type {RequestHandler & { upgrade: NonNullable }} */ (webSocketProxy).upgrade, ); - }, this); + } } /** @@ -2019,17 +2024,18 @@ class Server { '', ); + /** + * @type {StatsCompilation[]} + */ const statsForPrint = typeof (/** @type {MultiStats} */ (stats).stats) !== "undefined" - ? /** @type {MultiStats} */ (stats).toJson().children + ? /** @type {NonNullable} */ + (/** @type {MultiStats} */ (stats).toJson().children) : [/** @type {Stats} */ (stats).toJson()]; res.write(`

Assets Report:

`); - /** - * @type {StatsCompilation[]} - */ - (statsForPrint).forEach((item, index) => { + for (const [index, item] of statsForPrint.entries()) { res.write("
"); const name = @@ -2044,10 +2050,11 @@ class Server { res.write("
    "); const publicPath = item.publicPath === "auto" ? "" : item.publicPath; + const assets = + /** @type {NonNullable} */ + (item.assets); - for (const asset of /** @type {NonNullable} */ ( - item.assets - )) { + for (const asset of assets) { const assetName = asset.name; const assetURL = `${publicPath}${assetName}`; @@ -2060,7 +2067,7 @@ class Server { res.write("
"); res.write("
"); - }); + } res.end(""); }); @@ -2075,11 +2082,11 @@ class Server { const watchFiles = /** @type {NormalizedStatic[]} */ (this.options.static); if (watchFiles.length > 0) { - watchFiles.forEach((staticOption) => { - if (staticOption.watch) { - this.watchFiles(staticOption.directory, staticOption.watch); + for (const item of watchFiles) { + if (item.watch) { + this.watchFiles(item.directory, item.watch); } - }); + } } } @@ -2091,9 +2098,9 @@ class Server { const watchFiles = /** @type {WatchFiles[]} */ (this.options.watchFiles); if (watchFiles.length > 0) { - watchFiles.forEach((item) => { + for (const item of watchFiles) { this.watchFiles(item.paths, item.options); - }); + } } } @@ -2290,10 +2297,13 @@ class Server { }); } - if (/** @type {NormalizedStatic[]} */ (this.options.static).length > 0) { + const staticOptions = /** @type {NormalizedStatic[]} */ - (this.options.static).forEach((staticOption) => { - staticOption.publicPath.forEach((publicPath) => { + (this.options.static); + + if (staticOptions.length > 0) { + for (const staticOption of staticOptions) { + for (const publicPath of staticOption.publicPath) { middlewares.push({ name: "express-static", path: publicPath, @@ -2302,8 +2312,8 @@ class Server { staticOption.staticOptions, ), }); - }); - }); + } + } } if (this.options.historyApiFallback) { @@ -2345,10 +2355,9 @@ class Server { (this.middleware), }); - if (/** @type {NormalizedStatic[]} */ (this.options.static).length > 0) { - /** @type {NormalizedStatic[]} */ - (this.options.static).forEach((staticOption) => { - staticOption.publicPath.forEach((publicPath) => { + if (staticOptions.length > 0) { + for (const staticOption of staticOptions) { + for (const publicPath of staticOption.publicPath) { middlewares.push({ name: "express-static", path: publicPath, @@ -2357,17 +2366,16 @@ class Server { staticOption.staticOptions, ), }); - }); - }); + } + } } } - if (/** @type {NormalizedStatic[]} */ (this.options.static).length > 0) { + if (staticOptions.length > 0) { const serveIndex = require("serve-index"); - /** @type {NormalizedStatic[]} */ - (this.options.static).forEach((staticOption) => { - staticOption.publicPath.forEach((publicPath) => { + for (const staticOption of staticOptions) { + for (const publicPath of staticOption.publicPath) { if (staticOption.serveIndex) { middlewares.push({ name: "serve-index", @@ -2392,8 +2400,8 @@ class Server { }, }); } - }); - }); + } + } } // Register this middleware always as the last one so that it's only used as a @@ -2421,7 +2429,7 @@ class Server { middlewares = this.options.setupMiddlewares(middlewares, this); } - middlewares.forEach((middleware) => { + for (const middleware of middlewares) { if (typeof middleware === "function") { /** @type {A} */ (this.app).use( @@ -2442,7 +2450,7 @@ class Server { (middleware.middleware), ); } - }); + } } /** @@ -2962,6 +2970,8 @@ class Server { */ const allHeaders = []; + allHeaders.push({ key: "X_TEST", value: "TEST" }); + if (!Array.isArray(headers)) { // eslint-disable-next-line guard-for-in for (const name in headers) { @@ -2972,14 +2982,9 @@ class Server { headers = allHeaders; } - headers.forEach( - /** - * @param {{key: string, value: any}} header - */ - (header) => { - res.setHeader(header.key, header.value); - }, - ); + for (const { key, value } of headers) { + res.setHeader(key, value); + } } next(); diff --git a/test/fixtures/watch-files-config/webpack.config.js b/test/fixtures/watch-files-config/webpack.config.js index c31d06071d..2f64765a90 100644 --- a/test/fixtures/watch-files-config/webpack.config.js +++ b/test/fixtures/watch-files-config/webpack.config.js @@ -4,6 +4,7 @@ const HTMLGeneratorPlugin = require("../../helpers/html-generator-plugin"); module.exports = { mode: "development", + devtool: false, context: __dirname, stats: "none", entry: "./foo.js", From 4d44e436eccb3c78fee6faf41695d61e9cf994c5 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Fri, 16 Aug 2024 15:07:23 +0300 Subject: [PATCH 07/11] refactor: code --- lib/Server.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Server.js b/lib/Server.js index 21bfda572c..589a4fa8cf 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -1371,8 +1371,8 @@ class Server { for (const item of options.open) { if (typeof item === "string") { result.push({ target: item, options: defaultOpenOptions }); - - return; + // eslint-disable-next-line no-continue + continue; } result.push(...getOpenItemsFromObject(item)); From 0d653da9e695f03625face9241744633350cdc87 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Fri, 16 Aug 2024 15:16:03 +0300 Subject: [PATCH 08/11] refactor: code --- lib/Server.js | 46 ++++++++++++---------------------------------- 1 file changed, 12 insertions(+), 34 deletions(-) diff --git a/lib/Server.js b/lib/Server.js index 589a4fa8cf..a7d9c3d9e7 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -1106,15 +1106,13 @@ class Server { }, }; + const serverOptions = /** @type {ServerOptions} */ (options.server.options); + if ( options.server.type === "spdy" && - typeof (/** @type {ServerOptions} */ (options.server.options).spdy) === - "undefined" + typeof serverOptions.spdy === "undefined" ) { - /** @type {ServerOptions} */ - (options.server.options).spdy = { - protocols: ["h2", "http/1.1"], - }; + serverOptions.spdy = { protocols: ["h2", "http/1.1"] }; } if ( @@ -1122,13 +1120,8 @@ class Server { options.server.type === "http2" || options.server.type === "spdy" ) { - if ( - typeof ( - /** @type {ServerOptions} */ (options.server.options).requestCert - ) === "undefined" - ) { - /** @type {ServerOptions} */ - (options.server.options).requestCert = false; + if (typeof serverOptions.requestCert === "undefined") { + serverOptions.requestCert = false; } const httpsProperties = @@ -1136,19 +1129,13 @@ class Server { (["ca", "cert", "crl", "key", "pfx"]); for (const property of httpsProperties) { - if ( - typeof ( - /** @type {ServerOptions} */ (options.server.options)[property] - ) === "undefined" - ) { + if (typeof serverOptions[property] === "undefined") { // eslint-disable-next-line no-continue continue; } /** @type {any} */ - const value = - /** @type {ServerOptions} */ - (options.server.options)[property]; + const value = serverOptions[property]; /** * @param {string | Buffer | undefined} item * @returns {string | Buffer | undefined} @@ -1176,17 +1163,14 @@ class Server { }; /** @type {any} */ - (options.server.options)[property] = Array.isArray(value) + (serverOptions)[property] = Array.isArray(value) ? value.map((item) => readFile(item)) : readFile(value); } let fakeCert; - if ( - !(/** @type {ServerOptions} */ (options.server.options).key) || - !(/** @type {ServerOptions} */ (options.server.options).cert) - ) { + if (!serverOptions.key || !serverOptions.cert) { const certificateDir = Server.findCacheDir(); const certificatePath = path.join(certificateDir, "server.pem"); let certificateExists; @@ -1299,14 +1283,8 @@ class Server { this.logger.info(`SSL certificate: ${certificatePath}`); } - /** @type {ServerOptions} */ - (options.server.options).key = - /** @type {ServerOptions} */ - (options.server.options).key || fakeCert; - /** @type {ServerOptions} */ - (options.server.options).cert = - /** @type {ServerOptions} */ - (options.server.options).cert || fakeCert; + serverOptions.key = serverOptions.key || fakeCert; + serverOptions.cert = serverOptions.cert || fakeCert; } if (typeof options.ipc === "boolean") { From cd2e0e6b1fb031c559c50209c724738db26fa3d2 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Fri, 16 Aug 2024 15:41:59 +0300 Subject: [PATCH 09/11] refactor: ts --- lib/Server.js | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/lib/Server.js b/lib/Server.js index a7d9c3d9e7..985a90b5e5 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -31,6 +31,7 @@ const schema = require("./options.json"); /** @typedef {import("ipaddr.js").IPv4} IPv4 */ /** @typedef {import("ipaddr.js").IPv6} IPv6 */ /** @typedef {import("net").Socket} Socket */ +/** @typedef {import("http").Server} HTTPServer*/ /** @typedef {import("http").IncomingMessage} IncomingMessage */ /** @typedef {import("http").ServerResponse} ServerResponse */ /** @typedef {import("open").Options} OpenOptions */ @@ -199,8 +200,11 @@ const schema = require("./options.json"); * @typedef {{ name?: string, path?: string, middleware: MiddlewareHandler } | MiddlewareHandler } Middleware */ +/** @typedef {import("net").Server} BasicServer */ + /** * @template {BasicApplication} [A=ExpressApplication] + * @template {BasicServer} [S=import("http").Server] * @typedef {Object} Configuration * @property {boolean | string} [ipc] * @property {Host} [host] @@ -222,8 +226,8 @@ const schema = require("./options.json"); * @property {boolean} [setupExitSignals] * @property {boolean | ClientConfiguration} [client] * @property {Headers | ((req: Request, res: Response, context: DevMiddlewareContext) => Headers)} [headers] - * @property {(devServer: Server
) => void} [onListening] - * @property {(middlewares: Middleware[], devServer: Server) => Middleware[]} [setupMiddlewares] + * @property {(devServer: Server) => void} [onListening] + * @property {(middlewares: Middleware[], devServer: Server) => Middleware[]} [setupMiddlewares] */ if (!process.env.WEBPACK_SERVE) { @@ -302,11 +306,11 @@ function useFn(route, fn) { /** * @template {BasicApplication} [A=ExpressApplication] - * @template {import("http").Server} [S=import("http").Server] + * @template {BasicServer} [S=HTTPServer] */ class Server { /** - * @param {Configuration} options + * @param {Configuration} options * @param {Compiler | MultiCompiler} compiler */ constructor(options = {}, compiler) { @@ -1587,8 +1591,9 @@ class Server { } /** + * @template T * @private - * @returns {string} + * @returns {T} */ getServerTransport() { let implementation; @@ -1618,9 +1623,8 @@ class Server { try { // eslint-disable-next-line import/no-dynamic-require implementation = require( - /** @type {WebSocketServerConfiguration} */ ( - this.options.webSocketServer - ).type, + /** @type {WebSocketServerConfiguration} */ + (this.options.webSocketServer).type, ); } catch (error) { implementationFound = false; @@ -1628,9 +1632,9 @@ class Server { } break; case "function": - implementation = /** @type {WebSocketServerConfiguration} */ ( - this.options.webSocketServer - ).type; + implementation = + /** @type {WebSocketServerConfiguration} */ + (this.options.webSocketServer).type; break; default: implementationFound = false; @@ -2486,9 +2490,8 @@ class Server { */ createWebSocketServer() { /** @type {WebSocketServerImplementation | undefined | null} */ - this.webSocketServer = new /** @type {any} */ (this.getServerTransport())( - this, - ); + this.webSocketServer = new (this.getServerTransport())(this); + /** @type {WebSocketServerImplementation} */ (this.webSocketServer).implementation.on( "connection", From 23b6d9f1ef4dd9830f45aa8e7227aa352f757b4e Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Fri, 16 Aug 2024 15:42:33 +0300 Subject: [PATCH 10/11] refactor: ts --- types/lib/Server.d.ts | 140 ++++++++++++++++++++++-------------------- 1 file changed, 73 insertions(+), 67 deletions(-) diff --git a/types/lib/Server.d.ts b/types/lib/Server.d.ts index e52b2e5276..05e3aec532 100644 --- a/types/lib/Server.d.ts +++ b/types/lib/Server.d.ts @@ -5,11 +5,11 @@ export = Server; */ /** * @template {BasicApplication} [A=ExpressApplication] - * @template {import("http").Server} [S=import("http").Server] + * @template {BasicServer} [S=HTTPServer] */ declare class Server< A extends BasicApplication = import("express").Application, - S extends import("http").Server = import("http").Server< + S extends BasicServer = import("http").Server< typeof import("http").IncomingMessage, typeof import("http").ServerResponse >, @@ -1165,11 +1165,11 @@ declare class Server< */ private static isWebTarget; /** - * @param {Configuration} options + * @param {Configuration} options * @param {Compiler | MultiCompiler} compiler */ constructor( - options: Configuration | undefined, + options: Configuration | undefined, compiler: Compiler | MultiCompiler, ); compiler: import("webpack").Compiler | import("webpack").MultiCompiler; @@ -1177,7 +1177,7 @@ declare class Server< * @type {ReturnType} * */ logger: ReturnType; - options: Configuration; + options: Configuration; /** * @type {FSWatcher[]} */ @@ -1222,8 +1222,9 @@ declare class Server< */ private getClientTransport; /** + * @template T * @private - * @returns {string} + * @returns {T} */ private getServerTransport; /** @@ -1431,6 +1432,7 @@ declare namespace Server { IPv4, IPv6, Socket, + HTTPServer, IncomingMessage, ServerResponse, OpenOptions, @@ -1472,6 +1474,7 @@ declare namespace Server { Headers, MiddlewareHandler, Middleware, + BasicServer, Configuration, BasicApplication, }; @@ -1502,6 +1505,7 @@ type ServeStaticOptions = import("serve-static").ServeStaticOptions; type IPv4 = import("ipaddr.js").IPv4; type IPv6 = import("ipaddr.js").IPv6; type Socket = import("net").Socket; +type HTTPServer = import("http").Server; type IncomingMessage = import("http").IncomingMessage; type ServerResponse = import("http").ServerResponse; type OpenOptions = import("open").Options; @@ -1686,67 +1690,69 @@ type Middleware = middleware: MiddlewareHandler; } | MiddlewareHandler; -type Configuration = - { - ipc?: string | boolean | undefined; - host?: string | undefined; - port?: Port | undefined; - hot?: boolean | "only" | undefined; - liveReload?: boolean | undefined; - devMiddleware?: - | DevMiddlewareOptions< - import("express").Request< - import("express-serve-static-core").ParamsDictionary, - any, - any, - qs.ParsedQs, - Record - >, - import("express").Response> - > - | undefined; - compress?: boolean | undefined; - allowedHosts?: string | string[] | undefined; - historyApiFallback?: - | boolean - | import("connect-history-api-fallback").Options - | undefined; - bonjour?: - | boolean - | Record - | import("bonjour-service").Service - | undefined; - watchFiles?: - | string - | string[] - | WatchFiles - | (string | WatchFiles)[] - | undefined; - static?: string | boolean | Static | (string | Static)[] | undefined; - server?: string | ServerConfiguration | undefined; - app?: (() => Promise) | undefined; - webSocketServer?: - | string - | boolean - | WebSocketServerConfiguration - | undefined; - proxy?: ProxyConfigArray | undefined; - open?: string | boolean | Open | (string | Open)[] | undefined; - setupExitSignals?: boolean | undefined; - client?: boolean | ClientConfiguration | undefined; - headers?: - | Headers - | (( - req: Request, - res: Response, - context: DevMiddlewareContext, - ) => Headers) - | undefined; - onListening?: ((devServer: Server) => void) | undefined; - setupMiddlewares?: - | ((middlewares: Middleware[], devServer: Server) => Middleware[]) - | undefined; - }; +type BasicServer = import("net").Server; +type Configuration< + A extends BasicApplication = import("express").Application, + S extends BasicServer = import("http").Server< + typeof import("http").IncomingMessage, + typeof import("http").ServerResponse + >, +> = { + ipc?: string | boolean | undefined; + host?: string | undefined; + port?: Port | undefined; + hot?: boolean | "only" | undefined; + liveReload?: boolean | undefined; + devMiddleware?: + | DevMiddlewareOptions< + import("express").Request< + import("express-serve-static-core").ParamsDictionary, + any, + any, + qs.ParsedQs, + Record + >, + import("express").Response> + > + | undefined; + compress?: boolean | undefined; + allowedHosts?: string | string[] | undefined; + historyApiFallback?: + | boolean + | import("connect-history-api-fallback").Options + | undefined; + bonjour?: + | boolean + | Record + | import("bonjour-service").Service + | undefined; + watchFiles?: + | string + | string[] + | WatchFiles + | (string | WatchFiles)[] + | undefined; + static?: string | boolean | Static | (string | Static)[] | undefined; + server?: string | ServerConfiguration | undefined; + app?: (() => Promise) | undefined; + webSocketServer?: string | boolean | WebSocketServerConfiguration | undefined; + proxy?: ProxyConfigArray | undefined; + open?: string | boolean | Open | (string | Open)[] | undefined; + setupExitSignals?: boolean | undefined; + client?: boolean | ClientConfiguration | undefined; + headers?: + | Headers + | (( + req: Request, + res: Response, + context: DevMiddlewareContext, + ) => Headers) + | undefined; + onListening?: ((devServer: Server) => void) | undefined; + setupMiddlewares?: + | ((middlewares: Middleware[], devServer: Server) => Middleware[]) + | undefined; +}; type BasicApplication = { use: typeof useFn; }; From 889afb7e7aa7b790785bdd79480138df4feb9e54 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Fri, 16 Aug 2024 16:39:12 +0300 Subject: [PATCH 11/11] refactor: patch status message --- lib/Server.js | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/lib/Server.js b/lib/Server.js index 985a90b5e5..ba85abd41e 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -2096,12 +2096,30 @@ class Server { */ let middlewares = []; + const isHTTP2 = + /** @type {ServerConfiguration} */ (this.options.server).type === "http2"; + + if (isHTTP2) { + // TODO patch for https://github.com/pillarjs/finalhandler/pull/45, need remove then will be resolved + middlewares.push({ + name: "http2-status-message-patch", + middleware: + /** @type {NextHandleFunction} */ + (_req, res, next) => { + Object.defineProperty(res, "statusMessage", { + get() { + return ""; + }, + set() {}, + }); + + next(); + }, + }); + } + // compress is placed last and uses unshift so that it will be the first middleware used - if ( - this.options.compress && - /** @type {ServerConfiguration} */ - (this.options.server).type !== "http2" - ) { + if (this.options.compress && !isHTTP2) { const compression = require("compression"); middlewares.push({ name: "compression", middleware: compression() });