diff --git a/.cspell.json b/.cspell.json index d18a2d74b..b77825dec 100644 --- a/.cspell.json +++ b/.cspell.json @@ -22,7 +22,8 @@ "deoptimize", "etag", "cachable", - "finalhandler" + "finalhandler", + "hono" ], "ignorePaths": [ "CHANGELOG.md", diff --git a/README.md b/README.md index d5c5d1843..99c8e0f88 100644 --- a/README.md +++ b/README.md @@ -624,8 +624,8 @@ app.listen(3000, () => console.log("Example app listening on port 3000!")); ```js const Koa = require("koa"); const webpack = require("webpack"); -const webpackConfig = require("./test/fixtures/webpack.simple.config"); -const middleware = require("./dist"); +const webpackConfig = require("./webpack.simple.config"); +const middleware = require("webpack-dev-middleware"); const compiler = webpack(webpackConfig); const devMiddlewareOptions = { @@ -694,6 +694,27 @@ const devMiddlewareOptions = { })(); ``` +### Hono + +```js +import webpack from "webpack"; +import { serve } from "@hono/node-server"; +import { Hono } from "hono"; +import devMiddleware from "webpack-dev-middleware"; +import webpackConfig from "./webpack.config.js"; + +const compiler = webpack(webpackConfig); +const devMiddlewareOptions = { + /** Your webpack-dev-middleware-options */ +}; + +const app = new Hono(); + +app.use(devMiddleware.honoWrapper(compiler, devMiddlewareOptions)); + +serve(app); +``` + ## Contributing Please take a moment to read our contributing guidelines if you haven't yet done so. diff --git a/package-lock.json b/package-lock.json index e430a54d7..fd6a71ca0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@commitlint/config-conventional": "^19.0.3", "@fastify/express": "^2.3.0", "@hapi/hapi": "^21.3.7", + "@hono/node-server": "^1.12.0", "@types/connect": "^3.4.35", "@types/express": "^4.17.13", "@types/mime-types": "^2.1.1", @@ -46,6 +47,7 @@ "fastify": "^4.26.2", "file-loader": "^6.2.0", "finalhandler": "^1.2.0", + "hono": "^4.4.13", "husky": "^9.0.10", "jest": "^29.3.1", "joi": "^17.12.2", @@ -3485,6 +3487,15 @@ "@hapi/hoek": "^11.0.2" } }, + "node_modules/@hono/node-server": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.12.0.tgz", + "integrity": "sha512-e6oHjNiErRxsZRZBmc2KucuvY3btlO/XPncIpP2X75bRdTilF9GLjm3NHvKKunpJbbJJj31/FoPTksTf8djAVw==", + "dev": true, + "engines": { + "node": ">=18.14.1" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -11105,6 +11116,15 @@ "node": ">=8" } }, + "node_modules/hono": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.4.13.tgz", + "integrity": "sha512-c6qqenclmQ6wpXzqiElMa2jt423PVCmgBreDfC5s2lPPpGk7d0lOymd8QTzFZyYC5mSSs6imiTMPip+gLwuW/g==", + "dev": true, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", diff --git a/package.json b/package.json index 399e536aa..64f13bb66 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "@commitlint/config-conventional": "^19.0.3", "@fastify/express": "^2.3.0", "@hapi/hapi": "^21.3.7", + "@hono/node-server": "^1.12.0", "@types/connect": "^3.4.35", "@types/express": "^4.17.13", "@types/mime-types": "^2.1.1", @@ -90,6 +91,7 @@ "fastify": "^4.26.2", "file-loader": "^6.2.0", "finalhandler": "^1.2.0", + "hono": "^4.4.13", "husky": "^9.0.10", "jest": "^29.3.1", "joi": "^17.12.2", diff --git a/src/index.js b/src/index.js index 1fc9ccdff..f8c5ea0fb 100644 --- a/src/index.js +++ b/src/index.js @@ -249,15 +249,15 @@ function wdm(compiler, options = {}) { if ( Array.isArray(/** @type {MultiCompiler} */ (context.compiler).compilers) ) { - const compiler = /** @type {MultiCompiler} */ (context.compiler); - const watchOptions = compiler.compilers.map( + const c = /** @type {MultiCompiler} */ (context.compiler); + const watchOptions = c.compilers.map( (childCompiler) => childCompiler.options.watchOptions || {}, ); context.watching = compiler.watch(watchOptions, errorHandler); } else { - const compiler = /** @type {Compiler} */ (context.compiler); - const watchOptions = compiler.options.watchOptions || {}; + const c = /** @type {Compiler} */ (context.compiler); + const watchOptions = c.options.watchOptions || {}; context.watching = compiler.watch(watchOptions, errorHandler); } @@ -335,13 +335,35 @@ function hapiWrapper() { // @ts-ignore server.ext("onRequest", (request, h) => new Promise((resolve, reject) => { + let isFinished = false; + + /** + * @param {string | Buffer} [data] + */ + // eslint-disable-next-line no-param-reassign + request.raw.res.send = (data) => { + isFinished = true; + request.raw.res.end(data); + }; + + /** + * @param {string | Buffer} [data] + */ + // eslint-disable-next-line no-param-reassign + request.raw.res.finish = (data) => { + isFinished = true; + request.raw.res.end(data); + }; + devMiddleware(request.raw.req, request.raw.res, (error) => { if (error) { reject(error); return; } - resolve(request); + if (!isFinished) { + resolve(request); + } }); }) .then(() => h.continue) @@ -366,56 +388,88 @@ function koaWrapper(compiler, options) { const devMiddleware = wdm(compiler, options); /** - * @param {{ req: RequestInternal, res: ResponseInternal & import("./utils/compatibleAPI").ExpectedResponse, status: number, body: Buffer | import("fs").ReadStream | { message: string }, state: Object }} ctx + * @param {{ req: RequestInternal, res: ResponseInternal & import("./utils/compatibleAPI").ExpectedServerResponse, status: number, body: string | Buffer | import("fs").ReadStream | { message: string }, state: Object }} ctx * @param {Function} next * @returns {Promise} */ + const wrapper = async function webpackDevMiddleware(ctx, next) { - return new Promise((resolve, reject) => { - const { req } = ctx; - const { res } = ctx; - - res.locals = ctx.state; - /** - * @param {number} status status code - */ - res.status = (status) => { - // eslint-disable-next-line no-param-reassign - ctx.status = status; - }; - /** - * @param {import("fs").ReadStream} stream readable stream - */ - res.pipeInto = (stream) => { - // eslint-disable-next-line no-param-reassign - ctx.body = stream; - resolve(); - }; - /** - * @param {Buffer} content content - */ - res.send = (content) => { - // eslint-disable-next-line no-param-reassign - ctx.body = content; - resolve(); + const { req, res } = ctx; + + res.locals = ctx.state; + + let { status } = ctx; + + /** + * @returns {number} code + */ + res.getStatusCode = () => status; + + /** + * @param {number} statusCode status code + */ + res.setStatusCode = (statusCode) => { + status = statusCode; + // eslint-disable-next-line no-param-reassign + ctx.status = statusCode; + }; + + res.getReadyReadableStreamState = () => "open"; + + try { + await new Promise( + /** + * @param {(value: void) => void} resolve + * @param {(reason?: any) => void} reject + */ + (resolve, reject) => { + /** + * @param {import("fs").ReadStream} stream readable stream + */ + res.stream = (stream) => { + // eslint-disable-next-line no-param-reassign + ctx.body = stream; + }; + /** + * @param {string | Buffer} data data + */ + res.send = (data) => { + // eslint-disable-next-line no-param-reassign + ctx.body = data; + }; + + /** + * @param {string | Buffer} [data] data + */ + res.finish = (data) => { + // eslint-disable-next-line no-param-reassign + ctx.status = status; + res.end(data); + }; + + devMiddleware(req, res, (err) => { + if (err) { + reject(err); + return; + } + + resolve(); + }); + }, + ); + } catch (err) { + // eslint-disable-next-line no-param-reassign + ctx.status = + /** @type {Error & { statusCode: number }} */ (err).statusCode || + /** @type {Error & { status: number }} */ (err).status || + 500; + // eslint-disable-next-line no-param-reassign + ctx.body = { + message: /** @type {Error} */ (err).message, }; + } - devMiddleware(req, res, (err) => { - if (err) { - reject(err); - return; - } - - resolve(next()); - }).catch((err) => { - // eslint-disable-next-line no-param-reassign - ctx.status = err.statusCode || err.status || 500; - // eslint-disable-next-line no-param-reassign - ctx.body = { - message: err.message, - }; - }); - }); + await next(); }; wrapper.devMiddleware = devMiddleware; @@ -425,4 +479,153 @@ function koaWrapper(compiler, options) { wdm.koaWrapper = koaWrapper; +/** + * @template {IncomingMessage} [RequestInternal=IncomingMessage] + * @template {ServerResponse} [ResponseInternal=ServerResponse] + * @param {Compiler | MultiCompiler} compiler + * @param {Options} [options] + * @returns {(ctx: any, next: Function) => Promise | void} + */ +function honoWrapper(compiler, options) { + const devMiddleware = wdm(compiler, options); + + /** + * @param {{ env: any, body: any, json: any, status: any, set:any, req: RequestInternal & import("./utils/compatibleAPI").ExpectedIncomingMessage & { header: (name: string) => string }, res: ResponseInternal & import("./utils/compatibleAPI").ExpectedServerResponse & { headers: any, status: any } }} c + * @param {Function} next + * @returns {Promise} + */ + // eslint-disable-next-line consistent-return + const wrapper = async function webpackDevMiddleware(c, next) { + const { req, res } = c; + + c.set("webpack", { devMiddleware: devMiddleware.context }); + + /** + * @returns {string | undefined} + */ + req.getMethod = () => c.req.method; + + /** + * @param {string} name + * @returns {string | string[] | undefined} + */ + req.getHeader = (name) => c.req.header(name); + + /** + * @returns {string | undefined} + */ + req.getURL = () => c.req.url; + + let { status } = c.res; + + /** + * @returns {number} code + */ + res.getStatusCode = () => status; + + /** + * @param {number} code + */ + res.setStatusCode = (code) => { + status = code; + }; + + /** + * @param {string} name header name + */ + res.getHeader = (name) => c.res.headers.get(name); + + /** + * @param {string} name + * @param {string | number | Readonly} value + */ + res.setHeader = (name, value) => { + c.res.headers.append(name, value); + return c.res; + }; + + /** + * @param {string} name + */ + res.removeHeader = (name) => { + c.res.headers.delete(name); + }; + + /** + * @returns {string[]} + */ + res.getResponseHeaders = () => Array.from(c.res.headers.keys()); + + /** + * @returns {ServerResponse} + */ + res.getOutgoing = () => c.env.outgoing; + + res.setState = () => { + // Do nothing, because we set it before + }; + + res.getReadyReadableStreamState = () => "readable"; + + let body; + + try { + await new Promise( + /** + * @param {(value: void) => void} resolve + * @param {(reason?: any) => void} reject + */ + (resolve, reject) => { + /** + * @param {import("fs").ReadStream} stream readable stream + */ + res.stream = (stream) => { + body = stream; + // responseHandler(stream); + }; + + /** + * @param {string | Buffer} data data + */ + res.send = (data) => { + body = data; + }; + + /** + * @param {string | Buffer} [data] data + */ + res.finish = (data) => { + body = typeof data !== "undefined" ? data : null; + }; + + devMiddleware(req, res, (err) => { + if (err) { + reject(err); + return; + } + + resolve(); + }); + }, + ); + } catch (err) { + c.status(500); + + return c.json({ message: /** @type {Error} */ (err).message }); + } + + if (typeof body !== "undefined") { + return c.body(body, status); + } + + await next(); + }; + + wrapper.devMiddleware = devMiddleware; + + return wrapper; +} + +wdm.honoWrapper = honoWrapper; + module.exports = wdm; diff --git a/src/middleware.js b/src/middleware.js index a8bbe50ed..2438df9da 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -7,9 +7,22 @@ const onFinishedStream = require("on-finished"); const getFilenameFromUrl = require("./utils/getFilenameFromUrl"); const { setStatusCode, + getStatusCode, + getRequestHeader, + getRequestMethod, + getRequestURL, + getResponseHeader, + setResponseHeader, + removeResponseHeader, + getResponseHeaders, send, + finish, pipe, createReadStreamOrReadFileSync, + getOutgoing, + initState, + setState, + getReadyReadableStreamState, } = require("./utils/compatibleAPI"); const ready = require("./utils/ready"); const parseTokenList = require("./utils/parseTokenList"); @@ -123,9 +136,7 @@ function wrapper(context) { return async function middleware(req, res, next) { const acceptedMethods = context.options.methods || ["GET", "HEAD"]; - // fixes #282. credit @cexoso. in certain edge situations res.locals is undefined. - // eslint-disable-next-line no-param-reassign - res.locals = res.locals || {}; + initState(res); async function goNext() { if (!context.options.serverSideRender) { @@ -136,10 +147,7 @@ function wrapper(context) { ready( context, () => { - /** @type {any} */ - // eslint-disable-next-line no-param-reassign - (res.locals).webpack = { devMiddleware: context }; - + setState(res, "webpack", { devMiddleware: context }); resolve(next()); }, req, @@ -147,9 +155,10 @@ function wrapper(context) { }); } - if (req.method && !acceptedMethods.includes(req.method)) { - await goNext(); + const method = getRequestMethod(req); + if (method && !acceptedMethods.includes(method)) { + await goNext(); return; } @@ -177,10 +186,10 @@ function wrapper(context) { ); // Clear existing headers - const headers = res.getHeaderNames(); + const headers = getResponseHeaders(res); for (let i = 0; i < headers.length; i++) { - res.removeHeader(headers[i]); + removeResponseHeader(res, headers[i]); } if (options && options.headers) { @@ -191,16 +200,16 @@ function wrapper(context) { const value = options.headers[key]; if (typeof value !== "undefined") { - res.setHeader(key, value); + setResponseHeader(res, key, value); } } } // Send basic response setStatusCode(res, status); - res.setHeader("Content-Type", "text/html; charset=utf-8"); - res.setHeader("Content-Security-Policy", "default-src 'none'"); - res.setHeader("X-Content-Type-Options", "nosniff"); + setResponseHeader(res, "Content-Type", "text/html; charset=utf-8"); + setResponseHeader(res, "Content-Security-Policy", "default-src 'none'"); + setResponseHeader(res, "X-Content-Type-Options", "nosniff"); let byteLength = Buffer.byteLength(document); @@ -210,23 +219,43 @@ function wrapper(context) { (options.modifyResponseData(req, res, document, byteLength))); } - res.setHeader("Content-Length", byteLength); + setResponseHeader(res, "Content-Length", byteLength); - res.end(document); + finish(res, document); + } + + /** + * @param {NodeJS.ErrnoException} error + */ + function errorHandler(error) { + switch (error.code) { + case "ENAMETOOLONG": + case "ENOENT": + case "ENOTDIR": + sendError(404, { + modifyResponseData: context.options.modifyResponseData, + }); + break; + default: + sendError(500, { + modifyResponseData: context.options.modifyResponseData, + }); + break; + } } function isConditionalGET() { return ( - req.headers["if-match"] || - req.headers["if-unmodified-since"] || - req.headers["if-none-match"] || - req.headers["if-modified-since"] + getRequestHeader(req, "if-match") || + getRequestHeader(req, "if-unmodified-since") || + getRequestHeader(req, "if-none-match") || + getRequestHeader(req, "if-modified-since") ); } function isPreconditionFailure() { // if-match - const ifMatch = req.headers["if-match"]; + const ifMatch = /** @type {string} */ (getRequestHeader(req, "if-match")); // A recipient MUST ignore If-Unmodified-Since if the request contains // an If-Match header field; the condition in If-Match is considered to @@ -234,7 +263,7 @@ function wrapper(context) { // If-Unmodified-Since, and the two are only combined for the sake of // interoperating with older intermediaries that might not implement If-Match. if (ifMatch) { - const etag = res.getHeader("ETag"); + const etag = getResponseHeader(res, "ETag"); return ( !etag || @@ -249,7 +278,9 @@ function wrapper(context) { } // if-unmodified-since - const ifUnmodifiedSince = req.headers["if-unmodified-since"]; + const ifUnmodifiedSince = + /** @type {string} */ + (getRequestHeader(req, "if-unmodified-since")); if (ifUnmodifiedSince) { const unmodifiedSince = parseHttpDate(ifUnmodifiedSince); @@ -258,7 +289,7 @@ function wrapper(context) { // received field-value is not a valid HTTP-date. if (!isNaN(unmodifiedSince)) { const lastModified = parseHttpDate( - /** @type {string} */ (res.getHeader("Last-Modified")), + /** @type {string} */ (getResponseHeader(res, "Last-Modified")), ); return isNaN(lastModified) || lastModified > unmodifiedSince; @@ -272,9 +303,12 @@ function wrapper(context) { * @returns {boolean} is cachable */ function isCachable() { + const statusCode = getStatusCode(res); return ( - (res.statusCode >= 200 && res.statusCode < 300) || - res.statusCode === 304 + (statusCode >= 200 && statusCode < 300) || + statusCode === 304 || + // For Koa and Hono, because by default status code is 404, but we already found a file + statusCode === 404 ); } @@ -285,15 +319,21 @@ function wrapper(context) { function isFresh(resHeaders) { // Always return stale when Cache-Control: no-cache to support end-to-end reload requests // https://tools.ietf.org/html/rfc2616#section-14.9.4 - const cacheControl = req.headers["cache-control"]; + const cacheControl = + /** @type {string} */ + (getRequestHeader(req, "cache-control")); if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) { return false; } // fields - const noneMatch = req.headers["if-none-match"]; - const modifiedSince = req.headers["if-modified-since"]; + const noneMatch = + /** @type {string} */ + (getRequestHeader(req, "if-none-match")); + const modifiedSince = + /** @type {string} */ + (getRequestHeader(req, "if-modified-since")); // unconditional request if (!noneMatch && !modifiedSince) { @@ -357,7 +397,7 @@ function wrapper(context) { function isRangeFresh() { const ifRange = /** @type {string | undefined} */ - (req.headers["if-range"]); + (getRequestHeader(req, "if-range")); if (!ifRange) { return true; @@ -365,7 +405,9 @@ function wrapper(context) { // if-range as etag if (ifRange.indexOf('"') !== -1) { - const etag = /** @type {string | undefined} */ (res.getHeader("ETag")); + const etag = + /** @type {string | undefined} */ + (getResponseHeader(res, "ETag")); if (!etag) { return true; @@ -377,7 +419,7 @@ function wrapper(context) { // if-range as modified date const lastModified = /** @type {string | undefined} */ - (res.getHeader("Last-Modified")); + (getResponseHeader(res, "Last-Modified")); if (!lastModified) { return true; @@ -390,10 +432,10 @@ function wrapper(context) { * @returns {string | undefined} */ function getRangeHeader() { - const rage = req.headers.range; + const range = /** @type {string} */ (getRequestHeader(req, "range")); - if (rage && BYTES_RANGE_REGEXP.test(rage)) { - return rage; + if (range && BYTES_RANGE_REGEXP.test(range)) { + return range; } // eslint-disable-next-line no-undefined @@ -429,7 +471,7 @@ function wrapper(context) { const extra = {}; const filename = getFilenameFromUrl( context, - /** @type {string} */ (req.url), + /** @type {string} */ (getRequestURL(req)), extra, ); @@ -441,13 +483,12 @@ function wrapper(context) { sendError(extra.errorCode, { modifyResponseData: context.options.modifyResponseData, }); - + await goNext(); return; } if (!filename) { await goNext(); - return; } @@ -479,33 +520,44 @@ function wrapper(context) { } headers.forEach((header) => { - res.setHeader(header.key, header.value); + setResponseHeader(res, header.key, header.value); }); } - if (!res.getHeader("Content-Type")) { + if ( + !getResponseHeader(res, "Content-Type") || + getStatusCode(res) === 404 + ) { + removeResponseHeader(res, "Content-Type"); // content-type name(like application/javascript; charset=utf-8) or false const contentType = mime.contentType(path.extname(filename)); // Only set content-type header if media type is known // https://tools.ietf.org/html/rfc7231#section-3.1.1.5 if (contentType) { - res.setHeader("Content-Type", contentType); + setResponseHeader(res, "Content-Type", contentType); } else if (context.options.mimeTypeDefault) { - res.setHeader("Content-Type", context.options.mimeTypeDefault); + setResponseHeader( + res, + "Content-Type", + context.options.mimeTypeDefault, + ); } } - if (!res.getHeader("Accept-Ranges")) { - res.setHeader("Accept-Ranges", "bytes"); + if (!getResponseHeader(res, "Accept-Ranges")) { + setResponseHeader(res, "Accept-Ranges", "bytes"); } - if (context.options.lastModified && !res.getHeader("Last-Modified")) { + if ( + context.options.lastModified && + !getResponseHeader(res, "Last-Modified") + ) { const modified = /** @type {import("fs").Stats} */ (extra.stats).mtime.toUTCString(); - res.setHeader("Last-Modified", modified); + setResponseHeader(res, "Last-Modified", modified); } /** @type {number} */ @@ -520,7 +572,7 @@ function wrapper(context) { const rangeHeader = getRangeHeader(); - if (context.options.etag && !res.getHeader("ETag")) { + if (context.options.etag && !getResponseHeader(res, "ETag")) { /** @type {import("fs").Stats | Buffer | ReadStream | undefined} */ let value; @@ -554,8 +606,10 @@ function wrapper(context) { value = result.bufferOrStream; ({ bufferOrStream, byteLength } = result); - } catch (_err) { - // Ignore here + } catch (error) { + errorHandler(/** @type {NodeJS.ErrnoException} */ (error)); + await goNext(); + return; } } @@ -568,7 +622,7 @@ function wrapper(context) { bufferOrStream = result.buffer; } - res.setHeader("ETag", result.hash); + setResponseHeader(res, "ETag", result.hash); } } @@ -578,38 +632,38 @@ function wrapper(context) { sendError(412, { modifyResponseData: context.options.modifyResponseData, }); - + await goNext(); return; } - // For Koa - if (res.statusCode === 404) { - setStatusCode(res, 200); - } - if ( isCachable() && isFresh({ - etag: /** @type {string | undefined} */ (res.getHeader("ETag")), + etag: /** @type {string | undefined} */ ( + getResponseHeader(res, "ETag") + ), "last-modified": /** @type {string | undefined} */ - (res.getHeader("Last-Modified")), + (getResponseHeader(res, "Last-Modified")), }) ) { setStatusCode(res, 304); // Remove content header fields - res.removeHeader("Content-Encoding"); - res.removeHeader("Content-Language"); - res.removeHeader("Content-Length"); - res.removeHeader("Content-Range"); - res.removeHeader("Content-Type"); - res.end(); + removeResponseHeader(res, "Content-Encoding"); + removeResponseHeader(res, "Content-Language"); + removeResponseHeader(res, "Content-Length"); + removeResponseHeader(res, "Content-Range"); + removeResponseHeader(res, "Content-Type"); + finish(res); + await goNext(); return; } } + let isPartialContent = false; + if (rangeHeader) { let parsedRanges = /** @type {import("range-parser").Ranges | import("range-parser").Result | []} */ @@ -623,18 +677,19 @@ function wrapper(context) { if (parsedRanges === -1) { context.logger.error("Unsatisfiable range for 'Range' header."); - res.setHeader( + setResponseHeader( + res, "Content-Range", getValueContentRangeHeader("bytes", size), ); sendError(416, { headers: { - "Content-Range": res.getHeader("Content-Range"), + "Content-Range": getResponseHeader(res, "Content-Range"), }, modifyResponseData: context.options.modifyResponseData, }); - + await goNext(); return; } else if (parsedRanges === -2) { context.logger.error( @@ -649,7 +704,8 @@ function wrapper(context) { if (parsedRanges !== -2 && parsedRanges.length === 1) { // Content-Range setStatusCode(res, 206); - res.setHeader( + setResponseHeader( + res, "Content-Range", getValueContentRangeHeader( "bytes", @@ -658,6 +714,8 @@ function wrapper(context) { ), ); + isPartialContent = true; + [offset, len] = getOffsetAndLenFromRange(parsedRanges[0]); } } @@ -673,9 +731,9 @@ function wrapper(context) { start, end, )); - } catch (_ignoreError) { + } catch (error) { + errorHandler(/** @type {NodeJS.ErrnoException} */ (error)); await goNext(); - return; } } @@ -692,18 +750,22 @@ function wrapper(context) { } // @ts-ignore - res.setHeader("Content-Length", byteLength); + setResponseHeader(res, "Content-Length", byteLength); - if (req.method === "HEAD") { - // For Koa - if (res.statusCode === 404) { + if (method === "HEAD") { + if (!isPartialContent) { setStatusCode(res, 200); } - res.end(); + finish(res); + await goNext(); return; } + if (!isPartialContent) { + setStatusCode(res, 200); + } + const isPipeSupports = typeof ( /** @type {import("fs").ReadStream} */ (bufferOrStream).pipe @@ -711,6 +773,7 @@ function wrapper(context) { if (!isPipeSupports) { send(res, /** @type {Buffer} */ (bufferOrStream)); + await goNext(); return; } @@ -724,31 +787,25 @@ function wrapper(context) { // Error handling /** @type {import("fs").ReadStream} */ - (bufferOrStream).on("error", (error) => { - // clean up stream early - cleanup(); - - // Handle Error - switch (/** @type {NodeJS.ErrnoException} */ (error).code) { - case "ENAMETOOLONG": - case "ENOENT": - case "ENOTDIR": - sendError(404, { - modifyResponseData: context.options.modifyResponseData, - }); - break; - default: - sendError(500, { - modifyResponseData: context.options.modifyResponseData, - }); - break; - } - }); + (bufferOrStream) + .on("error", (error) => { + // clean up stream early + cleanup(); + errorHandler(error); + goNext(); + }) + .on(getReadyReadableStreamState(res), () => { + goNext(); + }); pipe(res, /** @type {ReadStream} */ (bufferOrStream)); - // Response finished, cleanup - onFinishedStream(res, cleanup); + const outgoing = getOutgoing(res); + + if (outgoing) { + // Response finished, cleanup + onFinishedStream(outgoing, cleanup); + } } ready(context, processRequest, req); diff --git a/src/utils/compatibleAPI.js b/src/utils/compatibleAPI.js index ab315b6f0..faa52cd70 100644 --- a/src/utils/compatibleAPI.js +++ b/src/utils/compatibleAPI.js @@ -2,21 +2,80 @@ /** @typedef {import("../index.js").ServerResponse} ServerResponse */ /** - * @typedef {Object} ExpectedResponse - * @property {(status: number) => void} [status] - * @property {(data: any) => void} [send] - * @property {(data: any) => void} [pipeInto] + * @typedef {Object} ExpectedIncomingMessage + * @property {(name: string) => string | string[] | undefined} [getHeader] + * @property {() => string | undefined} [getMethod] + * @property {() => string | undefined} [getURL] */ /** - * @template {ServerResponse & ExpectedResponse} Response + * @typedef {Object} ExpectedServerResponse + * @property {(status: number) => void} [setStatusCode] + * @property {() => number} [getStatusCode] + * @property {(name: string) => string | string[] | undefined | number} [getHeader] + * @property {(name: string, value: number | string | Readonly) => ExpectedServerResponse} [setHeader] + * @property {(name: string) => void} [removeHeader] + * @property {(data: string | Buffer) => void} [send] + * @property {(data?: string | Buffer) => void} [finish] + * @property {() => string[]} [getResponseHeaders] + * @property {(data: any) => void} [stream] + * @property {() => any} [getOutgoing] + * @property {(name: string, value: any) => void} [setState] + * @property {() => "ready" | "open" | "readable"} [getReadyReadableStreamState] + */ + +/** + * @template {IncomingMessage & ExpectedIncomingMessage} Request + * @param {Request} req + * @param {string} name + * @returns {string | string[] | undefined} + */ +function getRequestHeader(req, name) { + // Pseudo API + if (typeof req.getHeader === "function") { + return req.getHeader(name); + } + + return req.headers[name]; +} + +/** + * @template {IncomingMessage & ExpectedIncomingMessage} Request + * @param {Request} req + * @returns {string | undefined} + */ +function getRequestMethod(req) { + // Pseudo API + if (typeof req.getMethod === "function") { + return req.getMethod(); + } + + return req.method; +} + +/** + * @template {IncomingMessage & ExpectedIncomingMessage} Request + * @param {Request} req + * @returns {string | undefined} + */ +function getRequestURL(req) { + // Pseudo API + if (typeof req.getURL === "function") { + return req.getURL(); + } + + return req.url; +} + +/** + * @template {ServerResponse & ExpectedServerResponse} Response * @param {Response} res * @param {number} code */ function setStatusCode(res, code) { // Pseudo API - if (typeof res.status === "function") { - res.status(code); + if (typeof res.setStatusCode === "function") { + res.setStatusCode(code); return; } @@ -27,18 +86,76 @@ function setStatusCode(res, code) { } /** - * @template {ServerResponse} Response - * @param {Response & ExpectedResponse} res + * @template {ServerResponse & ExpectedServerResponse} Response + * @param {Response} res + * @returns {number} + */ +function getStatusCode(res) { + // Pseudo API + if (typeof res.getStatusCode === "function") { + return res.getStatusCode(); + } + + return res.statusCode; +} + +/** + * @template {ServerResponse & ExpectedServerResponse} Response + * @param {Response} res + * @param {string} name + * @returns {string | string[] | undefined | number} + */ +function getResponseHeader(res, name) { + // Real and Pseudo API + return res.getHeader(name); +} + +/** + * @template {ServerResponse & ExpectedServerResponse} Response + * @param {Response} res + * @param {string} name + * @param {number | string | Readonly} value + * @returns {Response} + */ +function setResponseHeader(res, name, value) { + // Real and Pseudo API + return res.setHeader(name, value); +} + +/** + * @template {ServerResponse & ExpectedServerResponse} Response + * @param {Response} res + * @param {string} name + */ +function removeResponseHeader(res, name) { + // Real and Pseudo API + res.removeHeader(name); +} + +/** + * @template {ServerResponse & ExpectedServerResponse} Response + * @param {Response} res + * @returns {string[]} + */ +function getResponseHeaders(res) { + // Pseudo API + if (typeof res.getResponseHeaders === "function") { + return res.getResponseHeaders(); + } + + return res.getHeaderNames(); +} + +/** + * @template {ServerResponse & ExpectedServerResponse} Response + * @param {Response} res * @param {import("fs").ReadStream} bufferOrStream */ function pipe(res, bufferOrStream) { // Pseudo API and Koa API - if ( - typeof (/** @type {Response & ExpectedResponse} */ (res).pipeInto) === - "function" - ) { + if (typeof res.stream === "function") { // Writable stream into Readable stream - res.pipeInto(bufferOrStream); + res.stream(bufferOrStream); return; } @@ -47,19 +164,34 @@ function pipe(res, bufferOrStream) { } /** - * @template {IncomingMessage} Request - * @template {ServerResponse} Response - * @param {Response & ExpectedResponse} res - * @param {string | Buffer} bufferOrStream + * @template {ServerResponse & ExpectedServerResponse} Response + * @param {Response} res + * @param {string | Buffer} bufferOrString */ -function send(res, bufferOrStream) { +function send(res, bufferOrString) { // Pseudo API and Express API and Koa API if (typeof res.send === "function") { - res.send(bufferOrStream); + res.send(bufferOrString); + return; + } + + res.end(bufferOrString); +} + +/** + * @template {ServerResponse & ExpectedServerResponse} Response + * @param {Response} res + * @param {string | Buffer} [data] + */ +function finish(res, data) { + // Pseudo API and Express API and Koa API + if (typeof res.finish === "function") { + res.finish(data); return; } - res.end(bufferOrStream); + // Pseudo API and Express API and Koa API + res.end(data); } /** @@ -104,4 +236,82 @@ function createReadStreamOrReadFileSync( return { bufferOrStream, byteLength }; } -module.exports = { setStatusCode, send, pipe, createReadStreamOrReadFileSync }; +/** + * @template {ServerResponse & ExpectedServerResponse} Response + * @param {Response} res + * @returns {Response} res + */ +function getOutgoing(res) { + // Pseudo API and Express API and Koa API + if (typeof res.getOutgoing === "function") { + return res.getOutgoing(); + } + + return res; +} + +/** + * @template {ServerResponse & ExpectedServerResponse} Response + * @param {Response} res + */ +function initState(res) { + if (typeof res.setState === "function") { + return; + } + + // fixes #282. credit @cexoso. in certain edge situations res.locals is undefined. + // eslint-disable-next-line no-param-reassign + res.locals = res.locals || {}; +} + +/** + * @template {ServerResponse & ExpectedServerResponse} Response + * @param {Response} res + * @param {string} name + * @param {any} value + */ +function setState(res, name, value) { + if (typeof res.setState === "function") { + res.setState(name, value); + + return; + } + + /** @type {any} */ + // eslint-disable-next-line no-param-reassign + (res.locals)[name] = value; +} + +/** + * @template {ServerResponse & ExpectedServerResponse} Response + * @param {Response} res + * @returns {"ready" | "open" | "readable"} + */ +function getReadyReadableStreamState(res) { + // Pseudo API and Express API and Koa API + if (typeof res.getReadyReadableStreamState === "function") { + return res.getReadyReadableStreamState(); + } + + return "ready"; +} + +module.exports = { + setStatusCode, + getStatusCode, + getRequestHeader, + getRequestMethod, + getRequestURL, + getResponseHeader, + setResponseHeader, + removeResponseHeader, + getResponseHeaders, + pipe, + send, + finish, + createReadStreamOrReadFileSync, + getOutgoing, + initState, + setState, + getReadyReadableStreamState, +}; diff --git a/src/utils/getFilenameFromUrl.js b/src/utils/getFilenameFromUrl.js index 67852ee8b..32e4f465b 100644 --- a/src/utils/getFilenameFromUrl.js +++ b/src/utils/getFilenameFromUrl.js @@ -138,6 +138,7 @@ function getFilenameFromUrl(context, url, extra = {}) { filename = path.join(filename, indexValue); try { + // eslint-disable-next-line no-param-reassign extra.stats = /** @type {import("fs").statSync} */ (context.outputFileSystem.statSync)(filename); diff --git a/test/middleware.test.js b/test/middleware.test.js index b9d2fbba7..a52c4a77c 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -8,6 +8,8 @@ import finalhandler from "finalhandler"; import fastify from "fastify"; import koa from "koa"; import Hapi from "@hapi/hapi"; +import { Hono } from "hono"; +import { serve } from "@hono/node-server"; import request from "supertest"; import memfs, { createFsFromVolume, Volume } from "memfs"; import del from "del"; @@ -45,6 +47,8 @@ async function startServer(name, app) { return resolve(server); }); + } else if (name === "hono") { + const server = serve(app, () => resolve(server)); } else { const server = app.listen({ port: 3000 }, (error) => { if (error) { @@ -123,6 +127,27 @@ async function frameworkFactory( return [server, req, koaMiddleware.devMiddleware]; } + case "hono": { + // eslint-disable-next-line new-cap + const app = new framework(); + const server = await startServer(name, app); + const req = request(server); + const instance = middleware.honoWrapper(compiler, devMiddlewareOptions); + const middlewares = + typeof options.setupMiddlewares === "function" + ? options.setupMiddlewares([instance]) + : [instance]; + + for (const item of middlewares) { + if (item.route) { + app.use(item.route, item.fn); + } else { + app.use(item); + } + } + + return [server, req, instance.devMiddleware]; + } default: { const isFastify = name === "fastify"; const isRouter = name === "router"; @@ -217,6 +242,8 @@ function get404ContentTypeHeader(name) { return "application/json; charset=utf-8"; case "fastify": return "application/json; charset=utf-8"; + case "hono": + return "text/plain; charset=UTF-8"; default: return "text/html; charset=utf-8"; } @@ -240,13 +267,26 @@ function applyTestMiddleware(name, middlewares) { }, }); } else if (name === "koa") { - middlewares.push((ctx, next) => { + middlewares.push(async (ctx, next) => { if (ctx.request.url === "/file.jpg") { ctx.set("Content-Type", "text/html"); + // eslint-disable-next-line no-param-reassign ctx.body = "welcome"; } - next(); + await next(); + }); + } else if (name === "hono") { + // eslint-disable-next-line consistent-return + middlewares.push(async (c, next) => { + if (c.req.url.endsWith("/file.jpg")) { + c.header("Content-Type", "text/html"); + c.status(200); + + return c.body("welcome"); + } + + await next(); }); } else { middlewares.push({ @@ -282,6 +322,7 @@ describe.each([ ["fastify", fastify], ["koa", koa], ["hapi", Hapi], + ["hono", Hono], ])("%s framework:", (name, framework) => { describe("middleware", () => { let instance; @@ -1814,12 +1855,12 @@ describe.each([ setupMiddlewares: (middlewares) => { if (name === "koa") { middlewares.unshift(async (ctx, next) => { + await next(); + ctx.set( "Content-Type", "application/vnd.test+octet-stream", ); - - await next(); }); } else if (name === "hapi") { middlewares.unshift({ @@ -1838,7 +1879,17 @@ describe.each([ }, }, }); + } else if (name === "hono") { + middlewares.unshift(async (c, next) => { + await next(); + + c.header( + "Content-Type", + "application/vnd.test+octet-stream", + ); + }); } else { + // eslint-disable-next-line no-shadow middlewares.unshift((req, res, next) => { // Express API if (res.set) { @@ -2816,7 +2867,7 @@ describe.each([ }); it('should return the "500" code for the "GET" request to the "image.svg" file', async () => { - const response = await req.get("/image.svg").set("Range", "bytes=0-"); + const response = await req.get("/image.svg"); expect(response.statusCode).toEqual(500); expect(response.headers["content-type"]).toEqual( @@ -2892,7 +2943,7 @@ describe.each([ }); it('should return the "404" code for the "GET" request to the "image.svg" file', async () => { - const response = await req.get("/image.svg").set("Range", "bytes=0-"); + const response = await req.get("/image.svg"); expect(response.statusCode).toEqual(404); expect(response.headers["content-type"]).toEqual( @@ -2997,6 +3048,138 @@ describe.each([ expect(response.body).toEqual({}); }); }); + + describe("should handle custom fs errors and response 500 code without `fs.createReadStream`", () => { + let compiler; + + const outputPath = path.resolve( + __dirname, + "./outputs/basic-test-errors-500", + ); + + beforeAll(async () => { + compiler = getCompiler({ + ...webpackConfig, + output: { + filename: "bundle.js", + path: outputPath, + }, + }); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); + + instance.context.outputFileSystem.mkdirSync(outputPath, { + recursive: true, + }); + instance.context.outputFileSystem.writeFileSync( + path.resolve(outputPath, "image.svg"), + "svg image", + ); + + instance.context.outputFileSystem.readFileSync = + function readFileSync() { + throw new Error("test"); + }; + instance.context.outputFileSystem.createReadStream = null; + }); + + afterAll(async () => { + await close(server, instance); + }); + + it('should return the "500" code for the "GET" request to the "image.svg" file', async () => { + const response = await req.get("/image.svg"); + + expect(response.statusCode).toEqual(500); + expect(response.headers["content-type"]).toEqual( + "text/html; charset=utf-8", + ); + expect(response.text).toEqual( + "\n" + + '\n' + + "\n" + + '\n' + + "Error\n" + + "\n" + + "\n" + + "
Internal Server Error
\n" + + "\n" + + "", + ); + }); + }); + + describe("should handle known fs errors and response 404 code", () => { + let compiler; + + const outputPath = path.resolve( + __dirname, + "./outputs/basic-test-errors-404", + ); + + beforeAll(async () => { + compiler = getCompiler({ + ...webpackConfig, + output: { + filename: "bundle.js", + path: outputPath, + }, + }); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); + + instance.context.outputFileSystem.mkdirSync(outputPath, { + recursive: true, + }); + instance.context.outputFileSystem.writeFileSync( + path.resolve(outputPath, "image.svg"), + "svg image", + ); + + instance.context.outputFileSystem.readFileSync = + function readFileSync() { + const error = new Error("test"); + + error.code = "ENAMETOOLONG"; + + throw error; + }; + instance.context.outputFileSystem.createReadStream = null; + }); + + afterAll(async () => { + await close(server, instance); + }); + + it('should return the "404" code for the "GET" request to the "image.svg" file', async () => { + const response = await req.get("/image.svg"); + + expect(response.statusCode).toEqual(404); + expect(response.headers["content-type"]).toEqual( + "text/html; charset=utf-8", + ); + expect(response.text).toEqual( + "\n" + + '\n' + + "\n" + + '\n' + + "Error\n" + + "\n" + + "\n" + + "
Not Found
\n" + + "\n" + + "", + ); + }); + }); }); describe("mimeTypes option", () => { @@ -3860,7 +4043,7 @@ describe.each([ expect(response.statusCode).toEqual(404); }); - it('should return the "200" code for the "HEAD" request to the bundle file', async () => { + it('should return the "404" code for the "HEAD" request to the bundle file', async () => { const response = await req.head(`/public/bundle.js`); expect(response.statusCode).toEqual(404); @@ -4091,6 +4274,7 @@ describe.each([ it('should return the "200" code for the "GET" request to path not in outputFileSystem but not return headers', async () => { const res = await req.get("/file.jpg"); + expect(res.statusCode).toEqual(200); expect(res.headers["X-nonsense-1"]).toBeUndefined(); expect(res.headers["X-nonsense-2"]).toBeUndefined(); @@ -4167,6 +4351,12 @@ describe.each([ await next(); }); + } else if (name === "hono") { + middlewares.push((c) => { + locals = { webpack: c.get("webpack") }; + + return c.body("welcome", 200); + }); } else if (name === "hapi") { middlewares.push({ plugin: { diff --git a/types/index.d.ts b/types/index.d.ts index dc8a67f6d..89a6806fd 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -162,6 +162,7 @@ declare namespace wdm { export { hapiWrapper, koaWrapper, + honoWrapper, Schema, Compiler, MultiCompiler, @@ -238,6 +239,20 @@ declare function koaWrapper< compiler: Compiler | MultiCompiler, options?: Options | undefined, ): (ctx: any, next: Function) => Promise | void; +/** + * @template {IncomingMessage} [RequestInternal=IncomingMessage] + * @template {ServerResponse} [ResponseInternal=ServerResponse] + * @param {Compiler | MultiCompiler} compiler + * @param {Options} [options] + * @returns {(ctx: any, next: Function) => Promise | void} + */ +declare function honoWrapper< + RequestInternal extends IncomingMessage = import("http").IncomingMessage, + ResponseInternal extends ServerResponse = ServerResponse, +>( + compiler: Compiler | MultiCompiler, + options?: Options | undefined, +): (ctx: any, next: Function) => Promise | void; type Schema = import("schema-utils/declarations/validate").Schema; type Compiler = import("webpack").Compiler; type MultiCompiler = import("webpack").MultiCompiler; diff --git a/types/utils/compatibleAPI.d.ts b/types/utils/compatibleAPI.d.ts index b3d9d1a92..5f16f508e 100644 --- a/types/utils/compatibleAPI.d.ts +++ b/types/utils/compatibleAPI.d.ts @@ -1,45 +1,162 @@ export type IncomingMessage = import("../index.js").IncomingMessage; export type ServerResponse = import("../index.js").ServerResponse; -export type ExpectedResponse = { - status?: ((status: number) => void) | undefined; - send?: ((data: any) => void) | undefined; - pipeInto?: ((data: any) => void) | undefined; +export type ExpectedIncomingMessage = { + getHeader?: ((name: string) => string | string[] | undefined) | undefined; + getMethod?: (() => string | undefined) | undefined; + getURL?: (() => string | undefined) | undefined; }; +export type ExpectedServerResponse = { + setStatusCode?: ((status: number) => void) | undefined; + getStatusCode?: (() => number) | undefined; + getHeader?: + | ((name: string) => string | string[] | undefined | number) + | undefined; + setHeader?: + | (( + name: string, + value: number | string | Readonly, + ) => ExpectedServerResponse) + | undefined; + removeHeader?: ((name: string) => void) | undefined; + send?: ((data: string | Buffer) => void) | undefined; + finish?: ((data?: string | Buffer) => void) | undefined; + getResponseHeaders?: (() => string[]) | undefined; + stream?: ((data: any) => void) | undefined; + getOutgoing?: (() => any) | undefined; + setState?: ((name: string, value: any) => void) | undefined; + getReadyReadableStreamState?: + | (() => "ready" | "open" | "readable") + | undefined; +}; +/** + * @template {ServerResponse & ExpectedServerResponse} Response + * @param {Response} res + * @param {number} code + */ +export function setStatusCode< + Response extends ServerResponse & ExpectedServerResponse, +>(res: Response, code: number): void; +/** + * @template {ServerResponse & ExpectedServerResponse} Response + * @param {Response} res + * @returns {number} + */ +export function getStatusCode< + Response extends ServerResponse & ExpectedServerResponse, +>(res: Response): number; /** @typedef {import("../index.js").IncomingMessage} IncomingMessage */ /** @typedef {import("../index.js").ServerResponse} ServerResponse */ /** - * @typedef {Object} ExpectedResponse - * @property {(status: number) => void} [status] - * @property {(data: any) => void} [send] - * @property {(data: any) => void} [pipeInto] + * @typedef {Object} ExpectedIncomingMessage + * @property {(name: string) => string | string[] | undefined} [getHeader] + * @property {() => string | undefined} [getMethod] + * @property {() => string | undefined} [getURL] + */ +/** + * @typedef {Object} ExpectedServerResponse + * @property {(status: number) => void} [setStatusCode] + * @property {() => number} [getStatusCode] + * @property {(name: string) => string | string[] | undefined | number} [getHeader] + * @property {(name: string, value: number | string | Readonly) => ExpectedServerResponse} [setHeader] + * @property {(name: string) => void} [removeHeader] + * @property {(data: string | Buffer) => void} [send] + * @property {(data?: string | Buffer) => void} [finish] + * @property {() => string[]} [getResponseHeaders] + * @property {(data: any) => void} [stream] + * @property {() => any} [getOutgoing] + * @property {(name: string, value: any) => void} [setState] + * @property {() => "ready" | "open" | "readable"} [getReadyReadableStreamState] */ /** - * @template {ServerResponse & ExpectedResponse} Response + * @template {IncomingMessage & ExpectedIncomingMessage} Request + * @param {Request} req + * @param {string} name + * @returns {string | string[] | undefined} + */ +export function getRequestHeader< + Request extends IncomingMessage & ExpectedIncomingMessage, +>(req: Request, name: string): string | string[] | undefined; +/** + * @template {IncomingMessage & ExpectedIncomingMessage} Request + * @param {Request} req + * @returns {string | undefined} + */ +export function getRequestMethod< + Request extends IncomingMessage & ExpectedIncomingMessage, +>(req: Request): string | undefined; +/** + * @template {IncomingMessage & ExpectedIncomingMessage} Request + * @param {Request} req + * @returns {string | undefined} + */ +export function getRequestURL< + Request extends IncomingMessage & ExpectedIncomingMessage, +>(req: Request): string | undefined; +/** + * @template {ServerResponse & ExpectedServerResponse} Response * @param {Response} res - * @param {number} code + * @param {string} name + * @returns {string | string[] | undefined | number} */ -export function setStatusCode< - Response extends ServerResponse & ExpectedResponse, ->(res: Response, code: number): void; +export function getResponseHeader< + Response extends ServerResponse & ExpectedServerResponse, +>(res: Response, name: string): string | string[] | undefined | number; +/** + * @template {ServerResponse & ExpectedServerResponse} Response + * @param {Response} res + * @param {string} name + * @param {number | string | Readonly} value + * @returns {Response} + */ +export function setResponseHeader< + Response extends ServerResponse & ExpectedServerResponse, +>( + res: Response, + name: string, + value: number | string | Readonly, +): Response; /** - * @template {IncomingMessage} Request - * @template {ServerResponse} Response - * @param {Response & ExpectedResponse} res - * @param {string | Buffer} bufferOrStream + * @template {ServerResponse & ExpectedServerResponse} Response + * @param {Response} res + * @param {string} name + */ +export function removeResponseHeader< + Response extends ServerResponse & ExpectedServerResponse, +>(res: Response, name: string): void; +/** + * @template {ServerResponse & ExpectedServerResponse} Response + * @param {Response} res + * @returns {string[]} */ -export function send< - Request extends IncomingMessage, - Response extends ServerResponse, ->(res: Response & ExpectedResponse, bufferOrStream: string | Buffer): void; +export function getResponseHeaders< + Response extends ServerResponse & ExpectedServerResponse, +>(res: Response): string[]; /** - * @template {ServerResponse} Response - * @param {Response & ExpectedResponse} res + * @template {ServerResponse & ExpectedServerResponse} Response + * @param {Response} res * @param {import("fs").ReadStream} bufferOrStream */ -export function pipe( - res: Response & ExpectedResponse, +export function pipe( + res: Response, bufferOrStream: import("fs").ReadStream, ): void; +/** + * @template {ServerResponse & ExpectedServerResponse} Response + * @param {Response} res + * @param {string | Buffer} bufferOrString + */ +export function send( + res: Response, + bufferOrString: string | Buffer, +): void; +/** + * @template {ServerResponse & ExpectedServerResponse} Response + * @param {Response} res + * @param {string | Buffer} [data] + */ +export function finish< + Response extends ServerResponse & ExpectedServerResponse, +>(res: Response, data?: string | Buffer | undefined): void; /** * @param {string} filename * @param {import("../index").OutputFileSystem} outputFileSystem @@ -56,3 +173,35 @@ export function createReadStreamOrReadFileSync( bufferOrStream: Buffer | import("fs").ReadStream; byteLength: number; }; +/** + * @template {ServerResponse & ExpectedServerResponse} Response + * @param {Response} res + * @returns {Response} res + */ +export function getOutgoing< + Response extends ServerResponse & ExpectedServerResponse, +>(res: Response): Response; +/** + * @template {ServerResponse & ExpectedServerResponse} Response + * @param {Response} res + */ +export function initState< + Response extends ServerResponse & ExpectedServerResponse, +>(res: Response): void; +/** + * @template {ServerResponse & ExpectedServerResponse} Response + * @param {Response} res + * @param {string} name + * @param {any} value + */ +export function setState< + Response extends ServerResponse & ExpectedServerResponse, +>(res: Response, name: string, value: any): void; +/** + * @template {ServerResponse & ExpectedServerResponse} Response + * @param {Response} res + * @returns {"ready" | "open" | "readable"} + */ +export function getReadyReadableStreamState< + Response extends ServerResponse & ExpectedServerResponse, +>(res: Response): "ready" | "open" | "readable";