Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: test with connect #5156

Merged
merged 15 commits into from Apr 24, 2024
245 changes: 181 additions & 64 deletions lib/Server.js
Expand Up @@ -18,9 +18,6 @@ const schema = require("./options.json");
/** @typedef {import("webpack").Stats} Stats */
/** @typedef {import("webpack").MultiStats} MultiStats */
/** @typedef {import("os").NetworkInterfaceInfo} NetworkInterfaceInfo */
/** @typedef {import("express").NextFunction} NextFunction */
/** @typedef {import("express").RequestHandler} ExpressRequestHandler */
/** @typedef {import("express").ErrorRequestHandler} ExpressErrorRequestHandler */
/** @typedef {import("chokidar").WatchOptions} WatchOptions */
/** @typedef {import("chokidar").FSWatcher} FSWatcher */
/** @typedef {import("connect-history-api-fallback").Options} ConnectHistoryApiFallbackOptions */
Expand All @@ -37,11 +34,28 @@ const schema = require("./options.json");
/** @typedef {import("http").IncomingMessage} IncomingMessage */
/** @typedef {import("http").ServerResponse} ServerResponse */
/** @typedef {import("open").Options} OpenOptions */
/** @typedef {import("express").Application} ExpressApplication */
/** @typedef {import("express").RequestHandler} ExpressRequestHandler */
/** @typedef {import("express").ErrorRequestHandler} ExpressErrorRequestHandler */
/** @typedef {import("express").Request} ExpressRequest */
/** @typedef {import("express").Response} ExpressResponse */

/** @typedef {(err?: any) => void} NextFunction */
/** @typedef {(req: IncomingMessage, res: ServerResponse) => void} SimpleHandleFunction */
/** @typedef {(req: IncomingMessage, res: ServerResponse, next: NextFunction) => void} NextHandleFunction */
/** @typedef {(err: any, req: IncomingMessage, res: ServerResponse, next: NextFunction) => void} ErrorHandleFunction */
/** @typedef {SimpleHandleFunction | NextHandleFunction | ErrorHandleFunction} HandleFunction */

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

/** @typedef {import("express").Request} Request */
/** @typedef {import("express").Response} Response */
/**
* @template {BasicApplication} [T=ExpressApplication]
* @typedef {T extends ExpressApplication ? ExpressRequest : IncomingMessage} Request
*/
/**
* @template {BasicApplication} [T=ExpressApplication]
* @typedef {T extends ExpressApplication ? ExpressResponse : ServerResponse} Response
*/

/**
* @template {Request} T
Expand Down Expand Up @@ -173,10 +187,16 @@ const schema = require("./options.json");
*/

/**
* @typedef {{ name?: string, path?: string, middleware: ExpressRequestHandler | ExpressErrorRequestHandler } | ExpressRequestHandler | ExpressErrorRequestHandler} Middleware
* @template {BasicApplication} [T=ExpressApplication]
* @typedef {T extends ExpressApplication ? ExpressRequestHandler | ExpressErrorRequestHandler : HandleFunction} MiddlewareHandler
*/

/**
* @typedef {{ name?: string, path?: string, middleware: MiddlewareHandler } | MiddlewareHandler } Middleware
*/

/**
* @template {BasicApplication} [T=ExpressApplication]
* @typedef {Object} Configuration
* @property {boolean | string} [ipc]
* @property {Host} [host]
Expand All @@ -191,16 +211,16 @@ const schema = require("./options.json");
* @property {string | string[] | WatchFiles | Array<string | WatchFiles>} [watchFiles]
* @property {boolean | string | Static | Array<string | Static>} [static]
* @property {boolean | ServerOptions} [https]
* @property {boolean} [http2]
* @property {"http" | "https" | "spdy" | string | ServerConfiguration} [server]
* @property {() => Promise<T>} [app]
* @property {boolean | "sockjs" | "ws" | string | WebSocketServerConfiguration} [webSocketServer]
* @property {ProxyConfigArray} [proxy]
* @property {boolean | string | Open | Array<string | Open>} [open]
* @property {boolean} [setupExitSignals]
* @property {boolean | ClientConfiguration} [client]
* @property {Headers | ((req: Request, res: Response, context: DevMiddlewareContext<Request, Response>) => Headers)} [headers]
* @property {(devServer: Server) => void} [onListening]
* @property {(middlewares: Middleware[], devServer: Server) => Middleware[]} [setupMiddlewares]
* @property {(devServer: Server<T>) => void} [onListening]
* @property {(middlewares: Middleware[], devServer: Server<T>) => Middleware[]} [setupMiddlewares]
*/

if (!process.env.WEBPACK_SERVE) {
Expand Down Expand Up @@ -245,23 +265,58 @@ const encodeOverlaySettings = (setting) =>
? encodeURIComponent(setting.toString())
: setting;

// Working for overload, because typescript doesn't support this yes
/**
* @overload
* @param {NextHandleFunction} fn
* @returns {BasicApplication}
*/
/**
* @overload
* @param {HandleFunction} fn
* @returns {BasicApplication}
*/
/**
* @overload
* @param {string} route
* @param {NextHandleFunction} fn
* @returns {BasicApplication}
*/
/**
* @param {string} route
* @param {HandleFunction} fn
* @returns {BasicApplication}
*/
// eslint-disable-next-line no-unused-vars
function useFn(route, fn) {
return /** @type {BasicApplication} */ ({});
}

/**
* @typedef {Object} BasicApplication
* @property {typeof useFn} use
*/

/**
* @template {BasicApplication} [T=ExpressApplication]
*/
class Server {
/**
* @param {Configuration | Compiler | MultiCompiler} options
* @param {Compiler | MultiCompiler | Configuration} compiler
* @param {Configuration<T>} options
* @param {Compiler | MultiCompiler} compiler
*/
constructor(options = {}, compiler) {
validate(/** @type {Schema} */ (schema), options, {
name: "Dev Server",
baseDataPath: "options",
});

this.compiler = /** @type {Compiler | MultiCompiler} */ (compiler);
this.compiler = compiler;
/**
* @type {ReturnType<Compiler["getInfrastructureLogger"]>}
* */
this.logger = this.compiler.getInfrastructureLogger("webpack-dev-server");
this.options = /** @type {Configuration} */ (options);
this.options = options;
/**
* @type {FSWatcher[]}
*/
Expand Down Expand Up @@ -1670,7 +1725,7 @@ class Server {
}

this.setupHooks();
this.setupApp();
await this.setupApp();
this.setupHostHeaderCheck();
this.setupDevMiddleware();
// Should be after `webpack-dev-middleware`, otherwise other middlewares might rewrite response
Expand Down Expand Up @@ -1729,11 +1784,14 @@ class Server {

/**
* @private
* @returns {void}
* @returns {Promise<void>}
*/
setupApp() {
/** @type {import("express").Application | undefined}*/
this.app = new /** @type {any} */ (getExpress())();
async setupApp() {
/** @type {T | undefined}*/
this.app =
typeof this.options.app === "function"
? await this.options.app()
: getExpress()();
}

/**
Expand Down Expand Up @@ -1788,29 +1846,22 @@ class Server {
* @returns {void}
*/
setupHostHeaderCheck() {
/** @type {import("express").Application} */
(this.app).all(
"*",
/**
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {void}
*/
(req, res, next) => {
if (
this.checkHeader(
/** @type {{ [key: string]: string | undefined }} */
(req.headers),
"host",
)
) {
return next();
}
/** @type {T} */
(this.app).use((req, res, next) => {
if (
this.checkHeader(
/** @type {{ [key: string]: string | undefined }} */
(req.headers),
"host",
)
) {
next();
return;
}

res.send("Invalid Host header");
},
);
res.statusCode = 403;
res.end("Invalid Host header");
});
}

/**
Expand All @@ -1834,45 +1885,103 @@ class Server {
setupBuiltInRoutes() {
const { app, middleware } = this;

/** @type {import("express").Application} */
(app).get("/__webpack_dev_server__/sockjs.bundle.js", (req, res) => {
res.setHeader("Content-Type", "application/javascript");
/** @type {T} */
(app).use("/__webpack_dev_server__/sockjs.bundle.js", (req, res, next) => {
if (req.method !== "GET" && req.method !== "HEAD") {
next();
return;
}

const clientPath = path.join(
__dirname,
"..",
"client/modules/sockjs-client/index.js",
);

// Express send Etag and other headers by default, so let's keep them for compatibility reasons
// @ts-ignore
if (typeof res.sendFile === "function") {
// @ts-ignore
res.sendFile(clientPath);
return;
}

let stats;

const clientPath = path.join(__dirname, "..", "client");
try {
// TODO implement `inputFileSystem.createReadStream` in webpack
stats = fs.statSync(clientPath);
} catch (err) {
next();
return;
}

res.sendFile(path.join(clientPath, "modules/sockjs-client/index.js"));
res.setHeader("Content-Type", "application/javascript; charset=UTF-8");
res.setHeader("Content-Length", stats.size);

if (req.method === "HEAD") {
res.end();
return;
}

fs.createReadStream(clientPath).pipe(res);
});

/** @type {import("express").Application} */
(app).get("/webpack-dev-server/invalidate", (_req, res) => {
/** @type {T} */
(app).use("/webpack-dev-server/invalidate", (req, res, next) => {
if (req.method !== "GET" && req.method !== "HEAD") {
next();
return;
}

this.invalidate();

res.end();
});

/** @type {import("express").Application} */
(app).get("/webpack-dev-server/open-editor", (req, res) => {
const fileName = req.query.fileName;
/** @type {T} */
(app).use("/webpack-dev-server/open-editor", (req, res, next) => {
if (req.method !== "GET" && req.method !== "HEAD") {
next();
return;
}

if (!req.url) {
next();
return;
}

const resolveUrl = new URL(req.url, `http://${req.headers.host}`);
const params = new URLSearchParams(resolveUrl.search);
const fileName = params.get("fileName");

if (typeof fileName === "string") {
// @ts-ignore
const launchEditor = require("launch-editor");

launchEditor(fileName);
}

res.end();
});

/** @type {import("express").Application} */
(app).get("/webpack-dev-server", (req, res) => {
/** @type {T} */
(app).use("/webpack-dev-server", (req, res, next) => {
if (req.method !== "GET" && req.method !== "HEAD") {
next();
return;
}

/** @type {import("webpack-dev-middleware").API<Request, Response>}*/
(middleware).waitUntilValid((stats) => {
res.setHeader("Content-Type", "text/html");
res.setHeader("Content-Type", "text/html; charset=utf-8");

// HEAD requests should not return body content
if (req.method === "HEAD") {
res.end();
return;
}

res.write(
'<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>',
);
Expand Down Expand Up @@ -1975,7 +2084,6 @@ class Server {
if (typeof this.options.headers !== "undefined") {
middlewares.push({
name: "set-headers",
path: "*",
middleware: this.setHeaders.bind(this),
});
}
Expand Down Expand Up @@ -2104,8 +2212,8 @@ class Server {

if (typeof bypassUrl === "boolean") {
// skip the proxy
// @ts-ignore
req.url = null;
res.statusCode = 404;
req.url = "";
next();
} else if (typeof bypassUrl === "string") {
// byPass to that url
Expand Down Expand Up @@ -2255,7 +2363,6 @@ class Server {
// fallback when no other middleware responses.
middlewares.push({
name: "options-middleware",
path: "*",
/**
* @param {Request} req
* @param {Response} res
Expand All @@ -2279,14 +2386,24 @@ class Server {

middlewares.forEach((middleware) => {
if (typeof middleware === "function") {
/** @type {import("express").Application} */
(this.app).use(middleware);
/** @type {T} */
(this.app).use(
/** @type {NextHandleFunction | HandleFunction} */
(middleware),
);
} else if (typeof middleware.path !== "undefined") {
/** @type {import("express").Application} */
(this.app).use(middleware.path, middleware.middleware);
/** @type {T} */
(this.app).use(
middleware.path,
/** @type {SimpleHandleFunction | NextHandleFunction} */
(middleware.middleware),
);
} else {
/** @type {import("express").Application} */
(this.app).use(middleware.middleware);
/** @type {T} */
(this.app).use(
/** @type {NextHandleFunction | HandleFunction} */
(middleware.middleware),
);
}
});
}
Expand Down Expand Up @@ -2794,7 +2911,7 @@ class Server {
headers = headers(
req,
res,
/** @type {import("webpack-dev-middleware").API<IncomingMessage, ServerResponse>}*/
/** @type {import("webpack-dev-middleware").API<Request, Response>}*/
(this.middleware).context,
);
}
Expand Down