Skip to content

Commit 3096148

Browse files
authoredApr 24, 2024··
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
}

‎lib/options.json

+8
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
"title": "Dev Server options",
33
"type": "object",
44
"definitions": {
5+
"App": {
6+
"instanceof": "Function",
7+
"description": "Allows to use custom applications (i.e. 'connect', 'fastify' and etc).",
8+
"link": " https://webpack.js.org/configuration/dev-server/#devserverapp"
9+
},
510
"AllowedHosts": {
611
"anyOf": [
712
{
@@ -997,6 +1002,9 @@
9971002
"server": {
9981003
"$ref": "#/definitions/Server"
9991004
},
1005+
"app": {
1006+
"$ref": "#/definitions/App"
1007+
},
10001008
"setupExitSignals": {
10011009
"$ref": "#/definitions/SetupExitSignals"
10021010
},

‎package-lock.json

+263-191
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+5-4
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,10 @@
9696
"babel-jest": "^29.5.0",
9797
"babel-loader": "^9.1.0",
9898
"body-parser": "^1.19.2",
99+
"connect": "^3.7.0",
99100
"core-js": "^3.31.0",
100101
"cspell": "^8.3.2",
101-
"css-loader": "^6.8.1",
102+
"css-loader": "^7.1.1",
102103
"eslint": "^8.43.0",
103104
"eslint-config-prettier": "^9.1.0",
104105
"eslint-config-webpack": "^1.2.5",
@@ -117,19 +118,19 @@
117118
"memfs": "^4.6.0",
118119
"npm-run-all": "^4.1.5",
119120
"prettier": "^3.2.4",
120-
"puppeteer": "^22.1.0",
121+
"puppeteer": "^22.6.5",
121122
"readable-stream": "^4.5.2",
122123
"require-from-string": "^2.0.2",
123124
"rimraf": "^5.0.5",
124125
"sockjs-client": "^1.6.1",
125126
"standard-version": "^9.3.0",
126127
"strip-ansi-v6": "npm:strip-ansi@^6.0.0",
127-
"style-loader": "^3.3.1",
128+
"style-loader": "^4.0.0",
128129
"supertest": "^6.1.3",
129130
"tcp-port-used": "^1.0.2",
130131
"typescript": "^5.3.3",
131132
"wait-for-expect": "^3.0.2",
132-
"webpack": "^5.89.0",
133+
"webpack": "^5.91.0",
133134
"webpack-cli": "^5.0.1",
134135
"webpack-merge": "^5.9.0"
135136
},

‎scripts/setupTest.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
process.env.CHOKIDAR_USEPOLLING = true;
44

5-
jest.setTimeout(300000);
5+
jest.setTimeout(400000);

‎test/__snapshots__/validate-options.test.js.snap.webpack5

+14
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,20 @@ exports[`options validate should throw an error on the "allowedHosts" option wit
5252
* options.allowedHosts should be a non-empty string."
5353
`;
5454

55+
exports[`options validate should throw an error on the "app" option with 'false' value 1`] = `
56+
"ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema.
57+
- options.app should be an instance of function.
58+
-> Allows to use custom applications (i.e. 'connect', 'fastify' and etc).
59+
-> Read more at https://webpack.js.org/configuration/dev-server/#devserverapp"
60+
`;
61+
62+
exports[`options validate should throw an error on the "app" option with 'test' value 1`] = `
63+
"ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema.
64+
- options.app should be an instance of function.
65+
-> Allows to use custom applications (i.e. 'connect', 'fastify' and etc).
66+
-> Read more at https://webpack.js.org/configuration/dev-server/#devserverapp"
67+
`;
68+
5569
exports[`options validate should throw an error on the "bonjour" option with '' value 1`] = `
5670
"ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema.
5771
- options.bonjour should be one of these:

‎test/e2e/__snapshots__/allowed-hosts.test.js.snap.webpack5

+12-4
Original file line numberDiff line numberDiff line change
@@ -262,15 +262,23 @@ exports[`allowed hosts should connect web socket client using localhost to web s
262262

263263
exports[`allowed hosts should connect web socket client using localhost to web socket server with the "auto" value ("ws"): page errors 1`] = `[]`;
264264

265-
exports[`allowed hosts should disconnect web client using localhost to web socket server with the "auto" value ("sockjs"): console messages 1`] = `[]`;
265+
exports[`allowed hosts should disconnect web client using localhost to web socket server with the "auto" value ("sockjs"): console messages 1`] = `
266+
[
267+
"Failed to load resource: the server responded with a status of 403 (Forbidden)",
268+
]
269+
`;
266270

267-
exports[`allowed hosts should disconnect web client using localhost to web socket server with the "auto" value ("sockjs"): html 1`] = `"<html><head></head><body>Invalid Host header</body></html>"`;
271+
exports[`allowed hosts should disconnect web client using localhost to web socket server with the "auto" value ("sockjs"): html 1`] = `"<html><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">Invalid Host header</pre></body></html>"`;
268272

269273
exports[`allowed hosts should disconnect web client using localhost to web socket server with the "auto" value ("sockjs"): page errors 1`] = `[]`;
270274

271-
exports[`allowed hosts should disconnect web client using localhost to web socket server with the "auto" value ("ws"): console messages 1`] = `[]`;
275+
exports[`allowed hosts should disconnect web client using localhost to web socket server with the "auto" value ("ws"): console messages 1`] = `
276+
[
277+
"Failed to load resource: the server responded with a status of 403 (Forbidden)",
278+
]
279+
`;
272280

273-
exports[`allowed hosts should disconnect web client using localhost to web socket server with the "auto" value ("ws"): html 1`] = `"<html><head></head><body>Invalid Host header</body></html>"`;
281+
exports[`allowed hosts should disconnect web client using localhost to web socket server with the "auto" value ("ws"): html 1`] = `"<html><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">Invalid Host header</pre></body></html>"`;
274282

275283
exports[`allowed hosts should disconnect web client using localhost to web socket server with the "auto" value ("ws"): page errors 1`] = `[]`;
276284

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`app option should work using "connect (async)" application and "http" server should handle GET request to index route (/): console messages 1`] = `
4+
[
5+
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
6+
"[HMR] Waiting for update signal from WDS...",
7+
"Hey.",
8+
]
9+
`;
10+
11+
exports[`app option should work using "connect (async)" application and "http" server should handle GET request to index route (/): page errors 1`] = `[]`;
12+
13+
exports[`app option should work using "connect (async)" application and "http" server should handle GET request to index route (/): response status 1`] = `200`;
14+
15+
exports[`app option should work using "connect (async)" application and "http" server should handle GET request to index route (/): response text 1`] = `
16+
"
17+
<!DOCTYPE html>
18+
<html>
19+
<head>
20+
<meta charset='UTF-8'>
21+
<title>webpack-dev-server</title>
22+
</head>
23+
<body>
24+
<h1>webpack-dev-server is running...</h1>
25+
<script type="text/javascript" charset="utf-8" src="/main.js"></script>
26+
</body>
27+
</html>
28+
"
29+
`;
30+
31+
exports[`app option should work using "connect (async)" application and "https" server should handle GET request to index route (/): console messages 1`] = `
32+
[
33+
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
34+
"[HMR] Waiting for update signal from WDS...",
35+
"Hey.",
36+
]
37+
`;
38+
39+
exports[`app option should work using "connect (async)" application and "https" server should handle GET request to index route (/): page errors 1`] = `[]`;
40+
41+
exports[`app option should work using "connect (async)" application and "https" server should handle GET request to index route (/): response status 1`] = `200`;
42+
43+
exports[`app option should work using "connect (async)" application and "https" server should handle GET request to index route (/): response text 1`] = `
44+
"
45+
<!DOCTYPE html>
46+
<html>
47+
<head>
48+
<meta charset='UTF-8'>
49+
<title>webpack-dev-server</title>
50+
</head>
51+
<body>
52+
<h1>webpack-dev-server is running...</h1>
53+
<script type="text/javascript" charset="utf-8" src="/main.js"></script>
54+
</body>
55+
</html>
56+
"
57+
`;
58+
59+
exports[`app option should work using "connect (async)" application and "spdy" server should handle GET request to index route (/): console messages 1`] = `
60+
[
61+
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
62+
"[HMR] Waiting for update signal from WDS...",
63+
"Hey.",
64+
]
65+
`;
66+
67+
exports[`app option should work using "connect (async)" application and "spdy" server should handle GET request to index route (/): page errors 1`] = `[]`;
68+
69+
exports[`app option should work using "connect (async)" application and "spdy" server should handle GET request to index route (/): response status 1`] = `200`;
70+
71+
exports[`app option should work using "connect (async)" application and "spdy" server should handle GET request to index route (/): response text 1`] = `
72+
"
73+
<!DOCTYPE html>
74+
<html>
75+
<head>
76+
<meta charset='UTF-8'>
77+
<title>webpack-dev-server</title>
78+
</head>
79+
<body>
80+
<h1>webpack-dev-server is running...</h1>
81+
<script type="text/javascript" charset="utf-8" src="/main.js"></script>
82+
</body>
83+
</html>
84+
"
85+
`;
86+
87+
exports[`app option should work using "connect" application and "http" server should handle GET request to index route (/): console messages 1`] = `
88+
[
89+
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
90+
"[HMR] Waiting for update signal from WDS...",
91+
"Hey.",
92+
]
93+
`;
94+
95+
exports[`app option should work using "connect" application and "http" server should handle GET request to index route (/): page errors 1`] = `[]`;
96+
97+
exports[`app option should work using "connect" application and "http" server should handle GET request to index route (/): response status 1`] = `200`;
98+
99+
exports[`app option should work using "connect" application and "http" server should handle GET request to index route (/): response text 1`] = `
100+
"
101+
<!DOCTYPE html>
102+
<html>
103+
<head>
104+
<meta charset='UTF-8'>
105+
<title>webpack-dev-server</title>
106+
</head>
107+
<body>
108+
<h1>webpack-dev-server is running...</h1>
109+
<script type="text/javascript" charset="utf-8" src="/main.js"></script>
110+
</body>
111+
</html>
112+
"
113+
`;
114+
115+
exports[`app option should work using "connect" application and "https" server should handle GET request to index route (/): console messages 1`] = `
116+
[
117+
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
118+
"[HMR] Waiting for update signal from WDS...",
119+
"Hey.",
120+
]
121+
`;
122+
123+
exports[`app option should work using "connect" application and "https" server should handle GET request to index route (/): page errors 1`] = `[]`;
124+
125+
exports[`app option should work using "connect" application and "https" server should handle GET request to index route (/): response status 1`] = `200`;
126+
127+
exports[`app option should work using "connect" application and "https" server should handle GET request to index route (/): response text 1`] = `
128+
"
129+
<!DOCTYPE html>
130+
<html>
131+
<head>
132+
<meta charset='UTF-8'>
133+
<title>webpack-dev-server</title>
134+
</head>
135+
<body>
136+
<h1>webpack-dev-server is running...</h1>
137+
<script type="text/javascript" charset="utf-8" src="/main.js"></script>
138+
</body>
139+
</html>
140+
"
141+
`;
142+
143+
exports[`app option should work using "connect" application and "spdy" server should handle GET request to index route (/): console messages 1`] = `
144+
[
145+
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
146+
"[HMR] Waiting for update signal from WDS...",
147+
"Hey.",
148+
]
149+
`;
150+
151+
exports[`app option should work using "connect" application and "spdy" server should handle GET request to index route (/): page errors 1`] = `[]`;
152+
153+
exports[`app option should work using "connect" application and "spdy" server should handle GET request to index route (/): response status 1`] = `200`;
154+
155+
exports[`app option should work using "connect" application and "spdy" server should handle GET request to index route (/): response text 1`] = `
156+
"
157+
<!DOCTYPE html>
158+
<html>
159+
<head>
160+
<meta charset='UTF-8'>
161+
<title>webpack-dev-server</title>
162+
</head>
163+
<body>
164+
<h1>webpack-dev-server is running...</h1>
165+
<script type="text/javascript" charset="utf-8" src="/main.js"></script>
166+
</body>
167+
</html>
168+
"
169+
`;
170+
171+
exports[`app option should work using "express" application and "http" server should handle GET request to index route (/): console messages 1`] = `
172+
[
173+
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
174+
"[HMR] Waiting for update signal from WDS...",
175+
"Hey.",
176+
]
177+
`;
178+
179+
exports[`app option should work using "express" application and "http" server should handle GET request to index route (/): page errors 1`] = `[]`;
180+
181+
exports[`app option should work using "express" application and "http" server should handle GET request to index route (/): response status 1`] = `200`;
182+
183+
exports[`app option should work using "express" application and "http" server should handle GET request to index route (/): response text 1`] = `
184+
"
185+
<!DOCTYPE html>
186+
<html>
187+
<head>
188+
<meta charset='UTF-8'>
189+
<title>webpack-dev-server</title>
190+
</head>
191+
<body>
192+
<h1>webpack-dev-server is running...</h1>
193+
<script type="text/javascript" charset="utf-8" src="/main.js"></script>
194+
</body>
195+
</html>
196+
"
197+
`;
198+
199+
exports[`app option should work using "express" application and "https" server should handle GET request to index route (/): console messages 1`] = `
200+
[
201+
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
202+
"[HMR] Waiting for update signal from WDS...",
203+
"Hey.",
204+
]
205+
`;
206+
207+
exports[`app option should work using "express" application and "https" server should handle GET request to index route (/): page errors 1`] = `[]`;
208+
209+
exports[`app option should work using "express" application and "https" server should handle GET request to index route (/): response status 1`] = `200`;
210+
211+
exports[`app option should work using "express" application and "https" server should handle GET request to index route (/): response text 1`] = `
212+
"
213+
<!DOCTYPE html>
214+
<html>
215+
<head>
216+
<meta charset='UTF-8'>
217+
<title>webpack-dev-server</title>
218+
</head>
219+
<body>
220+
<h1>webpack-dev-server is running...</h1>
221+
<script type="text/javascript" charset="utf-8" src="/main.js"></script>
222+
</body>
223+
</html>
224+
"
225+
`;
226+
227+
exports[`app option should work using "express" application and "spdy" server should handle GET request to index route (/): console messages 1`] = `
228+
[
229+
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
230+
"[HMR] Waiting for update signal from WDS...",
231+
"Hey.",
232+
]
233+
`;
234+
235+
exports[`app option should work using "express" application and "spdy" server should handle GET request to index route (/): page errors 1`] = `[]`;
236+
237+
exports[`app option should work using "express" application and "spdy" server should handle GET request to index route (/): response status 1`] = `200`;
238+
239+
exports[`app option should work using "express" application and "spdy" server should handle GET request to index route (/): response text 1`] = `
240+
"
241+
<!DOCTYPE html>
242+
<html>
243+
<head>
244+
<meta charset='UTF-8'>
245+
<title>webpack-dev-server</title>
246+
</head>
247+
<body>
248+
<h1>webpack-dev-server is running...</h1>
249+
<script type="text/javascript" charset="utf-8" src="/main.js"></script>
250+
</body>
251+
</html>
252+
"
253+
`;

‎test/e2e/__snapshots__/built-in-routes.test.js.snap.webpack5

+5-5
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ exports[`Built in routes with multi config should handle GET request to director
1616

1717
exports[`Built in routes with multi config should handle GET request to directory index and list all middleware directories: page errors 1`] = `[]`;
1818

19-
exports[`Built in routes with multi config should handle GET request to directory index and list all middleware directories: response headers content-type 1`] = `"text/html"`;
19+
exports[`Built in routes with multi config should handle GET request to directory index and list all middleware directories: response headers content-type 1`] = `"text/html; charset=utf-8"`;
2020

2121
exports[`Built in routes with multi config should handle GET request to directory index and list all middleware directories: response status 1`] = `200`;
2222

@@ -34,7 +34,7 @@ exports[`Built in routes with simple config should handle GET request to directo
3434

3535
exports[`Built in routes with simple config should handle GET request to directory index and list all middleware directories: page errors 1`] = `[]`;
3636

37-
exports[`Built in routes with simple config should handle GET request to directory index and list all middleware directories: response headers content-type 1`] = `"text/html"`;
37+
exports[`Built in routes with simple config should handle GET request to directory index and list all middleware directories: response headers content-type 1`] = `"text/html; charset=utf-8"`;
3838

3939
exports[`Built in routes with simple config should handle GET request to directory index and list all middleware directories: response status 1`] = `200`;
4040

@@ -56,7 +56,7 @@ exports[`Built in routes with simple config should handle HEAD request to direct
5656

5757
exports[`Built in routes with simple config should handle HEAD request to directory index: page errors 1`] = `[]`;
5858

59-
exports[`Built in routes with simple config should handle HEAD request to directory index: response headers content-type 1`] = `"text/html"`;
59+
exports[`Built in routes with simple config should handle HEAD request to directory index: response headers content-type 1`] = `"text/html; charset=utf-8"`;
6060

6161
exports[`Built in routes with simple config should handle HEAD request to directory index: response status 1`] = `200`;
6262

@@ -70,14 +70,14 @@ exports[`Built in routes with simple config should handles GET request to sockjs
7070

7171
exports[`Built in routes with simple config should handles GET request to sockjs bundle: page errors 1`] = `[]`;
7272

73-
exports[`Built in routes with simple config should handles GET request to sockjs bundle: response headers content-type 1`] = `"application/javascript"`;
73+
exports[`Built in routes with simple config should handles GET request to sockjs bundle: response headers content-type 1`] = `"application/javascript; charset=UTF-8"`;
7474

7575
exports[`Built in routes with simple config should handles GET request to sockjs bundle: response status 1`] = `200`;
7676

7777
exports[`Built in routes with simple config should handles HEAD request to sockjs bundle: console messages 1`] = `[]`;
7878

7979
exports[`Built in routes with simple config should handles HEAD request to sockjs bundle: page errors 1`] = `[]`;
8080

81-
exports[`Built in routes with simple config should handles HEAD request to sockjs bundle: response headers content-type 1`] = `"application/javascript"`;
81+
exports[`Built in routes with simple config should handles HEAD request to sockjs bundle: response headers content-type 1`] = `"application/javascript; charset=UTF-8"`;
8282

8383
exports[`Built in routes with simple config should handles HEAD request to sockjs bundle: response status 1`] = `200`;

‎test/e2e/app.test.js

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"use strict";
2+
3+
const path = require("path");
4+
const webpack = require("webpack");
5+
const Server = require("../../lib/Server");
6+
const config = require("../fixtures/client-config/webpack.config");
7+
const runBrowser = require("../helpers/run-browser");
8+
const port = require("../ports-map").app;
9+
10+
const staticDirectory = path.resolve(
11+
__dirname,
12+
"../fixtures/static-config/public",
13+
);
14+
15+
const apps = [
16+
["express", () => require("express")()],
17+
["connect", () => require("connect")()],
18+
["connect (async)", async () => require("express")()],
19+
];
20+
21+
const servers = ["http", "https", "spdy"];
22+
23+
describe("app option", () => {
24+
for (const [appName, app] of apps) {
25+
for (const server of servers) {
26+
let compiler;
27+
let devServer;
28+
let page;
29+
let browser;
30+
let pageErrors;
31+
let consoleMessages;
32+
33+
describe(`should work using "${appName}" application and "${server}" server`, () => {
34+
beforeEach(async () => {
35+
compiler = webpack(config);
36+
37+
devServer = new Server(
38+
{
39+
static: {
40+
directory: staticDirectory,
41+
watch: false,
42+
},
43+
app,
44+
server,
45+
port,
46+
},
47+
compiler,
48+
);
49+
50+
await devServer.start();
51+
52+
({ page, browser } = await runBrowser());
53+
54+
pageErrors = [];
55+
consoleMessages = [];
56+
});
57+
58+
afterEach(async () => {
59+
await browser.close();
60+
await devServer.stop();
61+
});
62+
63+
it("should handle GET request to index route (/)", async () => {
64+
page
65+
.on("console", (message) => {
66+
consoleMessages.push(message);
67+
})
68+
.on("pageerror", (error) => {
69+
pageErrors.push(error);
70+
});
71+
72+
const pageUrl =
73+
server === "https" || server === "spdy" || server === "http2"
74+
? `https://127.0.0.1:${port}/`
75+
: `http://127.0.0.1:${port}/`;
76+
77+
const response = await page.goto(pageUrl, {
78+
waitUntil: "networkidle0",
79+
});
80+
81+
const HTTPVersion = await page.evaluate(
82+
() => performance.getEntries()[0].nextHopProtocol,
83+
);
84+
85+
const isSpdy = server === "spdy";
86+
87+
if (isSpdy) {
88+
expect(HTTPVersion).toEqual("h2");
89+
} else {
90+
expect(HTTPVersion).toEqual("http/1.1");
91+
}
92+
93+
expect(response.status()).toMatchSnapshot("response status");
94+
expect(await response.text()).toMatchSnapshot("response text");
95+
expect(
96+
consoleMessages.map((message) => message.text()),
97+
).toMatchSnapshot("console messages");
98+
expect(pageErrors).toMatchSnapshot("page errors");
99+
});
100+
});
101+
}
102+
}
103+
});

‎test/e2e/on-listening.test.js

+12-6
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,18 @@ describe("onListening option", () => {
2626

2727
onListeningIsRunning = true;
2828

29-
devServer.app.get("/listening/some/path", (_, response) => {
30-
response.send("listening");
31-
});
32-
33-
devServer.app.post("/listening/some/path", (_, response) => {
34-
response.send("listening POST");
29+
devServer.app.use("/listening/some/path", (req, res, next) => {
30+
if (req.method === "GET") {
31+
res.setHeader("Content-Type", "text/html; charset=utf-8");
32+
res.end("listening");
33+
return;
34+
} else if (req.method === "POST") {
35+
res.setHeader("Content-Type", "text/html; charset=utf-8");
36+
res.end("listening POST");
37+
return;
38+
}
39+
40+
return next();
3541
});
3642
},
3743
port,

‎test/e2e/setup-middlewares.test.js

+19-11
Original file line numberDiff line numberDiff line change
@@ -23,35 +23,43 @@ describe("setupMiddlewares option", () => {
2323
throw new Error("webpack-dev-server is not defined");
2424
}
2525

26-
devServer.app.get("/setup-middleware/some/path", (_, response) => {
27-
response.send("setup-middlewares option GET");
28-
});
29-
30-
devServer.app.post("/setup-middleware/some/path", (_, response) => {
31-
response.send("setup-middlewares option POST");
26+
devServer.app.use("/setup-middleware/some/path", (req, res, next) => {
27+
if (req.method === "GET") {
28+
res.setHeader("Content-Type", "text/html; charset=utf-8");
29+
res.end("setup-middlewares option GET");
30+
return;
31+
} else if (req.method === "POST") {
32+
res.setHeader("Content-Type", "text/html; charset=utf-8");
33+
res.end("setup-middlewares option POST");
34+
return;
35+
}
36+
37+
return next();
3238
});
3339

3440
middlewares.push({
3541
name: "hello-world-test-two",
3642
middleware: (req, res, next) => {
37-
if (req.path !== "/foo/bar/baz") {
43+
if (req.url !== "/foo/bar/baz") {
3844
next();
39-
4045
return;
4146
}
4247

43-
res.send("Hello World without path!");
48+
res.setHeader("Content-Type", "text/html; charset=utf-8");
49+
res.end("Hello World without path!");
4450
},
4551
});
4652
middlewares.push({
4753
name: "hello-world-test-one",
4854
path: "/foo/bar",
4955
middleware: (req, res) => {
50-
res.send("Hello World with path!");
56+
res.setHeader("Content-Type", "text/html; charset=utf-8");
57+
res.end("Hello World with path!");
5158
},
5259
});
5360
middlewares.push((req, res) => {
54-
res.send("Hello World as function!");
61+
res.setHeader("Content-Type", "text/html; charset=utf-8");
62+
res.end("Hello World as function!");
5563
});
5664

5765
return middlewares;

‎test/ports-map.js

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ const listOfTests = {
8080
"normalize-option": 1,
8181
"setup-middlewares-option": 1,
8282
"options-request-response": 2,
83+
app: 1,
8384
};
8485

8586
let startPort = 8089;

‎test/server/proxy-option.test.js

+13-7
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const proxyOptionPathsAsProperties = [
2727
{
2828
context: "/foo",
2929
bypass(req) {
30-
if (/\.html$/.test(req.path)) {
30+
if (/\.html$/.test(req.path || req.url)) {
3131
return "/index.html";
3232
}
3333

@@ -37,15 +37,15 @@ const proxyOptionPathsAsProperties = [
3737
{
3838
context: "proxyfalse",
3939
bypass(req) {
40-
if (/\/proxyfalse$/.test(req.path)) {
40+
if (/\/proxyfalse$/.test(req.path || req.url)) {
4141
return false;
4242
}
4343
},
4444
},
4545
{
4646
context: "/proxy/async",
4747
bypass(req, res) {
48-
if (/\/proxy\/async$/.test(req.path)) {
48+
if (/\/proxy\/async$/.test(req.path || req.url)) {
4949
return new Promise((resolve) => {
5050
setTimeout(() => {
5151
res.end("proxy async response");
@@ -61,7 +61,7 @@ const proxyOptionPathsAsProperties = [
6161
changeOrigin: true,
6262
secure: false,
6363
bypass(req) {
64-
if (/\.(html)$/i.test(req.url)) {
64+
if (/\.(html)$/i.test(req.path || req.url)) {
6565
return req.url;
6666
}
6767
},
@@ -95,10 +95,16 @@ const proxyOptionOfArray = [
9595
target: `http://localhost:${port2}`,
9696
pathRewrite: { "^/api": "" },
9797
bypass: () => {
98-
if (req && req.query.foo) {
99-
res.end(`foo+${next.name}+${typeof next}`);
98+
if (req) {
99+
const resolveUrl = new URL(req.url, `http://${req.headers.host}`);
100+
const params = new URLSearchParams(resolveUrl.search);
101+
const foo = params.get("foo");
100102

101-
return false;
103+
if (foo) {
104+
res.end(`foo+${next.name}+${typeof next}`);
105+
106+
return false;
107+
}
102108
}
103109
},
104110
};

‎test/validate-options.test.js

+10
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,16 @@ const tests = {
437437
},
438438
],
439439
},
440+
app: {
441+
success: [
442+
() => require("connect")(),
443+
async () =>
444+
new Promise((resolve) => {
445+
resolve(require("connect")());
446+
}),
447+
],
448+
failure: ["test", false],
449+
},
440450
static: {
441451
success: [
442452
"path",

‎types/lib/Server.d.ts

+637-197
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.