Skip to content

Commit

Permalink
feat: added the app option to setup any connect compatibility HTT…
Browse files Browse the repository at this point in the history
…P server framework
  • Loading branch information
alexander-akait committed Apr 24, 2024
1 parent 1a1561f commit 3096148
Show file tree
Hide file tree
Showing 16 changed files with 1,537 additions and 490 deletions.
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

0 comments on commit 3096148

Please sign in to comment.