Skip to content

Commit 3096148

Browse files
feat: added the app option to setup any connect compatibility HTTP server framework
1 parent 1a1561f commit 3096148

16 files changed

+1537
-490
lines changed

lib/Server.js

+181-64
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@ const schema = require("./options.json");
1818
/** @typedef {import("webpack").Stats} Stats */
1919
/** @typedef {import("webpack").MultiStats} MultiStats */
2020
/** @typedef {import("os").NetworkInterfaceInfo} NetworkInterfaceInfo */
21-
/** @typedef {import("express").NextFunction} NextFunction */
22-
/** @typedef {import("express").RequestHandler} ExpressRequestHandler */
23-
/** @typedef {import("express").ErrorRequestHandler} ExpressErrorRequestHandler */
2421
/** @typedef {import("chokidar").WatchOptions} WatchOptions */
2522
/** @typedef {import("chokidar").FSWatcher} FSWatcher */
2623
/** @typedef {import("connect-history-api-fallback").Options} ConnectHistoryApiFallbackOptions */
@@ -37,11 +34,28 @@ const schema = require("./options.json");
3734
/** @typedef {import("http").IncomingMessage} IncomingMessage */
3835
/** @typedef {import("http").ServerResponse} ServerResponse */
3936
/** @typedef {import("open").Options} OpenOptions */
37+
/** @typedef {import("express").Application} ExpressApplication */
38+
/** @typedef {import("express").RequestHandler} ExpressRequestHandler */
39+
/** @typedef {import("express").ErrorRequestHandler} ExpressErrorRequestHandler */
40+
/** @typedef {import("express").Request} ExpressRequest */
41+
/** @typedef {import("express").Response} ExpressResponse */
42+
43+
/** @typedef {(err?: any) => void} NextFunction */
44+
/** @typedef {(req: IncomingMessage, res: ServerResponse) => void} SimpleHandleFunction */
45+
/** @typedef {(req: IncomingMessage, res: ServerResponse, next: NextFunction) => void} NextHandleFunction */
46+
/** @typedef {(err: any, req: IncomingMessage, res: ServerResponse, next: NextFunction) => void} ErrorHandleFunction */
47+
/** @typedef {SimpleHandleFunction | NextHandleFunction | ErrorHandleFunction} HandleFunction */
4048

4149
/** @typedef {import("https").ServerOptions & { spdy?: { plain?: boolean | undefined, ssl?: boolean | undefined, 'x-forwarded-for'?: string | undefined, protocol?: string | undefined, protocols?: string[] | undefined }}} ServerOptions */
4250

43-
/** @typedef {import("express").Request} Request */
44-
/** @typedef {import("express").Response} Response */
51+
/**
52+
* @template {BasicApplication} [T=ExpressApplication]
53+
* @typedef {T extends ExpressApplication ? ExpressRequest : IncomingMessage} Request
54+
*/
55+
/**
56+
* @template {BasicApplication} [T=ExpressApplication]
57+
* @typedef {T extends ExpressApplication ? ExpressResponse : ServerResponse} Response
58+
*/
4559

4660
/**
4761
* @template {Request} T
@@ -173,10 +187,16 @@ const schema = require("./options.json");
173187
*/
174188

175189
/**
176-
* @typedef {{ name?: string, path?: string, middleware: ExpressRequestHandler | ExpressErrorRequestHandler } | ExpressRequestHandler | ExpressErrorRequestHandler} Middleware
190+
* @template {BasicApplication} [T=ExpressApplication]
191+
* @typedef {T extends ExpressApplication ? ExpressRequestHandler | ExpressErrorRequestHandler : HandleFunction} MiddlewareHandler
192+
*/
193+
194+
/**
195+
* @typedef {{ name?: string, path?: string, middleware: MiddlewareHandler } | MiddlewareHandler } Middleware
177196
*/
178197

179198
/**
199+
* @template {BasicApplication} [T=ExpressApplication]
180200
* @typedef {Object} Configuration
181201
* @property {boolean | string} [ipc]
182202
* @property {Host} [host]
@@ -191,16 +211,16 @@ const schema = require("./options.json");
191211
* @property {string | string[] | WatchFiles | Array<string | WatchFiles>} [watchFiles]
192212
* @property {boolean | string | Static | Array<string | Static>} [static]
193213
* @property {boolean | ServerOptions} [https]
194-
* @property {boolean} [http2]
195214
* @property {"http" | "https" | "spdy" | string | ServerConfiguration} [server]
215+
* @property {() => Promise<T>} [app]
196216
* @property {boolean | "sockjs" | "ws" | string | WebSocketServerConfiguration} [webSocketServer]
197217
* @property {ProxyConfigArray} [proxy]
198218
* @property {boolean | string | Open | Array<string | Open>} [open]
199219
* @property {boolean} [setupExitSignals]
200220
* @property {boolean | ClientConfiguration} [client]
201221
* @property {Headers | ((req: Request, res: Response, context: DevMiddlewareContext<Request, Response>) => Headers)} [headers]
202-
* @property {(devServer: Server) => void} [onListening]
203-
* @property {(middlewares: Middleware[], devServer: Server) => Middleware[]} [setupMiddlewares]
222+
* @property {(devServer: Server<T>) => void} [onListening]
223+
* @property {(middlewares: Middleware[], devServer: Server<T>) => Middleware[]} [setupMiddlewares]
204224
*/
205225

206226
if (!process.env.WEBPACK_SERVE) {
@@ -245,23 +265,58 @@ const encodeOverlaySettings = (setting) =>
245265
? encodeURIComponent(setting.toString())
246266
: setting;
247267

268+
// Working for overload, because typescript doesn't support this yes
269+
/**
270+
* @overload
271+
* @param {NextHandleFunction} fn
272+
* @returns {BasicApplication}
273+
*/
274+
/**
275+
* @overload
276+
* @param {HandleFunction} fn
277+
* @returns {BasicApplication}
278+
*/
279+
/**
280+
* @overload
281+
* @param {string} route
282+
* @param {NextHandleFunction} fn
283+
* @returns {BasicApplication}
284+
*/
285+
/**
286+
* @param {string} route
287+
* @param {HandleFunction} fn
288+
* @returns {BasicApplication}
289+
*/
290+
// eslint-disable-next-line no-unused-vars
291+
function useFn(route, fn) {
292+
return /** @type {BasicApplication} */ ({});
293+
}
294+
295+
/**
296+
* @typedef {Object} BasicApplication
297+
* @property {typeof useFn} use
298+
*/
299+
300+
/**
301+
* @template {BasicApplication} [T=ExpressApplication]
302+
*/
248303
class Server {
249304
/**
250-
* @param {Configuration | Compiler | MultiCompiler} options
251-
* @param {Compiler | MultiCompiler | Configuration} compiler
305+
* @param {Configuration<T>} options
306+
* @param {Compiler | MultiCompiler} compiler
252307
*/
253308
constructor(options = {}, compiler) {
254309
validate(/** @type {Schema} */ (schema), options, {
255310
name: "Dev Server",
256311
baseDataPath: "options",
257312
});
258313

259-
this.compiler = /** @type {Compiler | MultiCompiler} */ (compiler);
314+
this.compiler = compiler;
260315
/**
261316
* @type {ReturnType<Compiler["getInfrastructureLogger"]>}
262317
* */
263318
this.logger = this.compiler.getInfrastructureLogger("webpack-dev-server");
264-
this.options = /** @type {Configuration} */ (options);
319+
this.options = options;
265320
/**
266321
* @type {FSWatcher[]}
267322
*/
@@ -1670,7 +1725,7 @@ class Server {
16701725
}
16711726

16721727
this.setupHooks();
1673-
this.setupApp();
1728+
await this.setupApp();
16741729
this.setupHostHeaderCheck();
16751730
this.setupDevMiddleware();
16761731
// Should be after `webpack-dev-middleware`, otherwise other middlewares might rewrite response
@@ -1729,11 +1784,14 @@ class Server {
17291784

17301785
/**
17311786
* @private
1732-
* @returns {void}
1787+
* @returns {Promise<void>}
17331788
*/
1734-
setupApp() {
1735-
/** @type {import("express").Application | undefined}*/
1736-
this.app = new /** @type {any} */ (getExpress())();
1789+
async setupApp() {
1790+
/** @type {T | undefined}*/
1791+
this.app =
1792+
typeof this.options.app === "function"
1793+
? await this.options.app()
1794+
: getExpress()();
17371795
}
17381796

17391797
/**
@@ -1788,29 +1846,22 @@ class Server {
17881846
* @returns {void}
17891847
*/
17901848
setupHostHeaderCheck() {
1791-
/** @type {import("express").Application} */
1792-
(this.app).all(
1793-
"*",
1794-
/**
1795-
* @param {Request} req
1796-
* @param {Response} res
1797-
* @param {NextFunction} next
1798-
* @returns {void}
1799-
*/
1800-
(req, res, next) => {
1801-
if (
1802-
this.checkHeader(
1803-
/** @type {{ [key: string]: string | undefined }} */
1804-
(req.headers),
1805-
"host",
1806-
)
1807-
) {
1808-
return next();
1809-
}
1849+
/** @type {T} */
1850+
(this.app).use((req, res, next) => {
1851+
if (
1852+
this.checkHeader(
1853+
/** @type {{ [key: string]: string | undefined }} */
1854+
(req.headers),
1855+
"host",
1856+
)
1857+
) {
1858+
next();
1859+
return;
1860+
}
18101861

1811-
res.send("Invalid Host header");
1812-
},
1813-
);
1862+
res.statusCode = 403;
1863+
res.end("Invalid Host header");
1864+
});
18141865
}
18151866

18161867
/**
@@ -1834,45 +1885,103 @@ class Server {
18341885
setupBuiltInRoutes() {
18351886
const { app, middleware } = this;
18361887

1837-
/** @type {import("express").Application} */
1838-
(app).get("/__webpack_dev_server__/sockjs.bundle.js", (req, res) => {
1839-
res.setHeader("Content-Type", "application/javascript");
1888+
/** @type {T} */
1889+
(app).use("/__webpack_dev_server__/sockjs.bundle.js", (req, res, next) => {
1890+
if (req.method !== "GET" && req.method !== "HEAD") {
1891+
next();
1892+
return;
1893+
}
1894+
1895+
const clientPath = path.join(
1896+
__dirname,
1897+
"..",
1898+
"client/modules/sockjs-client/index.js",
1899+
);
1900+
1901+
// Express send Etag and other headers by default, so let's keep them for compatibility reasons
1902+
// @ts-ignore
1903+
if (typeof res.sendFile === "function") {
1904+
// @ts-ignore
1905+
res.sendFile(clientPath);
1906+
return;
1907+
}
1908+
1909+
let stats;
18401910

1841-
const clientPath = path.join(__dirname, "..", "client");
1911+
try {
1912+
// TODO implement `inputFileSystem.createReadStream` in webpack
1913+
stats = fs.statSync(clientPath);
1914+
} catch (err) {
1915+
next();
1916+
return;
1917+
}
18421918

1843-
res.sendFile(path.join(clientPath, "modules/sockjs-client/index.js"));
1919+
res.setHeader("Content-Type", "application/javascript; charset=UTF-8");
1920+
res.setHeader("Content-Length", stats.size);
1921+
1922+
if (req.method === "HEAD") {
1923+
res.end();
1924+
return;
1925+
}
1926+
1927+
fs.createReadStream(clientPath).pipe(res);
18441928
});
18451929

1846-
/** @type {import("express").Application} */
1847-
(app).get("/webpack-dev-server/invalidate", (_req, res) => {
1930+
/** @type {T} */
1931+
(app).use("/webpack-dev-server/invalidate", (req, res, next) => {
1932+
if (req.method !== "GET" && req.method !== "HEAD") {
1933+
next();
1934+
return;
1935+
}
1936+
18481937
this.invalidate();
18491938

18501939
res.end();
18511940
});
18521941

1853-
/** @type {import("express").Application} */
1854-
(app).get("/webpack-dev-server/open-editor", (req, res) => {
1855-
const fileName = req.query.fileName;
1942+
/** @type {T} */
1943+
(app).use("/webpack-dev-server/open-editor", (req, res, next) => {
1944+
if (req.method !== "GET" && req.method !== "HEAD") {
1945+
next();
1946+
return;
1947+
}
1948+
1949+
if (!req.url) {
1950+
next();
1951+
return;
1952+
}
1953+
1954+
const resolveUrl = new URL(req.url, `http://${req.headers.host}`);
1955+
const params = new URLSearchParams(resolveUrl.search);
1956+
const fileName = params.get("fileName");
18561957

18571958
if (typeof fileName === "string") {
18581959
// @ts-ignore
18591960
const launchEditor = require("launch-editor");
1961+
18601962
launchEditor(fileName);
18611963
}
18621964

18631965
res.end();
18641966
});
18651967

1866-
/** @type {import("express").Application} */
1867-
(app).get("/webpack-dev-server", (req, res) => {
1968+
/** @type {T} */
1969+
(app).use("/webpack-dev-server", (req, res, next) => {
1970+
if (req.method !== "GET" && req.method !== "HEAD") {
1971+
next();
1972+
return;
1973+
}
1974+
18681975
/** @type {import("webpack-dev-middleware").API<Request, Response>}*/
18691976
(middleware).waitUntilValid((stats) => {
1870-
res.setHeader("Content-Type", "text/html");
1977+
res.setHeader("Content-Type", "text/html; charset=utf-8");
1978+
18711979
// HEAD requests should not return body content
18721980
if (req.method === "HEAD") {
18731981
res.end();
18741982
return;
18751983
}
1984+
18761985
res.write(
18771986
'<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>',
18781987
);
@@ -1975,7 +2084,6 @@ class Server {
19752084
if (typeof this.options.headers !== "undefined") {
19762085
middlewares.push({
19772086
name: "set-headers",
1978-
path: "*",
19792087
middleware: this.setHeaders.bind(this),
19802088
});
19812089
}
@@ -2104,8 +2212,8 @@ class Server {
21042212

21052213
if (typeof bypassUrl === "boolean") {
21062214
// skip the proxy
2107-
// @ts-ignore
2108-
req.url = null;
2215+
res.statusCode = 404;
2216+
req.url = "";
21092217
next();
21102218
} else if (typeof bypassUrl === "string") {
21112219
// byPass to that url
@@ -2255,7 +2363,6 @@ class Server {
22552363
// fallback when no other middleware responses.
22562364
middlewares.push({
22572365
name: "options-middleware",
2258-
path: "*",
22592366
/**
22602367
* @param {Request} req
22612368
* @param {Response} res
@@ -2279,14 +2386,24 @@ class Server {
22792386

22802387
middlewares.forEach((middleware) => {
22812388
if (typeof middleware === "function") {
2282-
/** @type {import("express").Application} */
2283-
(this.app).use(middleware);
2389+
/** @type {T} */
2390+
(this.app).use(
2391+
/** @type {NextHandleFunction | HandleFunction} */
2392+
(middleware),
2393+
);
22842394
} else if (typeof middleware.path !== "undefined") {
2285-
/** @type {import("express").Application} */
2286-
(this.app).use(middleware.path, middleware.middleware);
2395+
/** @type {T} */
2396+
(this.app).use(
2397+
middleware.path,
2398+
/** @type {SimpleHandleFunction | NextHandleFunction} */
2399+
(middleware.middleware),
2400+
);
22872401
} else {
2288-
/** @type {import("express").Application} */
2289-
(this.app).use(middleware.middleware);
2402+
/** @type {T} */
2403+
(this.app).use(
2404+
/** @type {NextHandleFunction | HandleFunction} */
2405+
(middleware.middleware),
2406+
);
22902407
}
22912408
});
22922409
}
@@ -2794,7 +2911,7 @@ class Server {
27942911
headers = headers(
27952912
req,
27962913
res,
2797-
/** @type {import("webpack-dev-middleware").API<IncomingMessage, ServerResponse>}*/
2914+
/** @type {import("webpack-dev-middleware").API<Request, Response>}*/
27982915
(this.middleware).context,
27992916
);
28002917
}

0 commit comments

Comments
 (0)