webpack-dev-middleware
Advanced tools
Comparing version 7.1.1 to 7.2.0
@@ -64,3 +64,3 @@ "use strict"; | ||
* @typedef {Object} ResponseData | ||
* @property {string | Buffer | ReadStream} data | ||
* @property {Buffer | ReadStream} data | ||
* @property {number} byteLength | ||
@@ -70,8 +70,8 @@ */ | ||
/** | ||
* @template {IncomingMessage} RequestInternal | ||
* @template {ServerResponse} ResponseInternal | ||
* @template {IncomingMessage} [RequestInternal=IncomingMessage] | ||
* @template {ServerResponse} [ResponseInternal=ServerResponse] | ||
* @callback ModifyResponseData | ||
* @param {RequestInternal} req | ||
* @param {ResponseInternal} res | ||
* @param {string | Buffer | ReadStream} data | ||
* @param {Buffer | ReadStream} data | ||
* @param {number} byteLength | ||
@@ -82,4 +82,4 @@ * @return {ResponseData} | ||
/** | ||
* @template {IncomingMessage} RequestInternal | ||
* @template {ServerResponse} ResponseInternal | ||
* @template {IncomingMessage} [RequestInternal=IncomingMessage] | ||
* @template {ServerResponse} [ResponseInternal=ServerResponse] | ||
* @typedef {Object} Context | ||
@@ -97,4 +97,4 @@ * @property {boolean} state | ||
/** | ||
* @template {IncomingMessage} RequestInternal | ||
* @template {ServerResponse} ResponseInternal | ||
* @template {IncomingMessage} [RequestInternal=IncomingMessage] | ||
* @template {ServerResponse} [ResponseInternal=ServerResponse] | ||
* @typedef {WithoutUndefined<Context<RequestInternal, ResponseInternal>, "watching">} FilledContext | ||
@@ -106,4 +106,4 @@ */ | ||
/** | ||
* @template {IncomingMessage} RequestInternal | ||
* @template {ServerResponse} ResponseInternal | ||
* @template {IncomingMessage} [RequestInternal=IncomingMessage] | ||
* @template {ServerResponse} [ResponseInternal=ServerResponse] | ||
* @typedef {NormalizedHeaders | ((req: RequestInternal, res: ResponseInternal, context: Context<RequestInternal, ResponseInternal>) => void | undefined | NormalizedHeaders) | undefined} Headers | ||
@@ -113,4 +113,4 @@ */ | ||
/** | ||
* @template {IncomingMessage} RequestInternal | ||
* @template {ServerResponse} ResponseInternal | ||
* @template {IncomingMessage} [RequestInternal = IncomingMessage] | ||
* @template {ServerResponse} [ResponseInternal = ServerResponse] | ||
* @typedef {Object} Options | ||
@@ -128,7 +128,9 @@ * @property {{[key: string]: string}} [mimeTypes] | ||
* @property {ModifyResponseData<RequestInternal, ResponseInternal>} [modifyResponseData] | ||
* @property {"weak" | "strong"} [etag] | ||
* @property {boolean} [lastModified] | ||
*/ | ||
/** | ||
* @template {IncomingMessage} RequestInternal | ||
* @template {ServerResponse} ResponseInternal | ||
* @template {IncomingMessage} [RequestInternal=IncomingMessage] | ||
* @template {ServerResponse} [ResponseInternal=ServerResponse] | ||
* @callback Middleware | ||
@@ -177,4 +179,4 @@ * @param {RequestInternal} req | ||
/** | ||
* @template {IncomingMessage} RequestInternal | ||
* @template {ServerResponse} ResponseInternal | ||
* @template {IncomingMessage} [RequestInternal=IncomingMessage] | ||
* @template {ServerResponse} [ResponseInternal=ServerResponse] | ||
* @typedef {Middleware<RequestInternal, ResponseInternal> & AdditionalMethods<RequestInternal, ResponseInternal>} API | ||
@@ -196,4 +198,4 @@ */ | ||
/** | ||
* @template {IncomingMessage} RequestInternal | ||
* @template {ServerResponse} ResponseInternal | ||
* @template {IncomingMessage} [RequestInternal=IncomingMessage] | ||
* @template {ServerResponse} [ResponseInternal=ServerResponse] | ||
* @param {Compiler | MultiCompiler} compiler | ||
@@ -289,2 +291,126 @@ * @param {Options<RequestInternal, ResponseInternal>} [options] | ||
} | ||
/** | ||
* @template S | ||
* @template O | ||
* @typedef {Object} HapiPluginBase | ||
* @property {(server: S, options: O) => void | Promise<void>} register | ||
*/ | ||
/** | ||
* @template S | ||
* @template O | ||
* @typedef {HapiPluginBase<S, O> & { pkg: { name: string } }} HapiPlugin | ||
*/ | ||
/** | ||
* @typedef {Options & { compiler: Compiler | MultiCompiler }} HapiOptions | ||
*/ | ||
/** | ||
* @template HapiServer | ||
* @template {HapiOptions} HapiOptionsInternal | ||
* @returns {HapiPlugin<HapiServer, HapiOptionsInternal>} | ||
*/ | ||
function hapiWrapper() { | ||
return { | ||
pkg: { | ||
name: "webpack-dev-middleware" | ||
}, | ||
register(server, options) { | ||
const { | ||
compiler, | ||
...rest | ||
} = options; | ||
if (!compiler) { | ||
throw new Error("The compiler options is required."); | ||
} | ||
const devMiddleware = wdm(compiler, rest); | ||
// @ts-ignore | ||
server.decorate("server", "webpackDevMiddleware", devMiddleware); | ||
// @ts-ignore | ||
server.ext("onRequest", (request, h) => new Promise((resolve, reject) => { | ||
devMiddleware(request.raw.req, request.raw.res, error => { | ||
if (error) { | ||
reject(error); | ||
return; | ||
} | ||
resolve(request); | ||
}); | ||
}).then(() => h.continue).catch(error => { | ||
throw error; | ||
})); | ||
} | ||
}; | ||
} | ||
wdm.hapiWrapper = hapiWrapper; | ||
/** | ||
* @template {IncomingMessage} [RequestInternal=IncomingMessage] | ||
* @template {ServerResponse} [ResponseInternal=ServerResponse] | ||
* @param {Compiler | MultiCompiler} compiler | ||
* @param {Options<RequestInternal, ResponseInternal>} [options] | ||
* @returns {(ctx: any, next: Function) => Promise<void> | void} | ||
*/ | ||
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 {Function} next | ||
* @returns {Promise<void>} | ||
*/ | ||
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(); | ||
}; | ||
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 | ||
}; | ||
}); | ||
}); | ||
}; | ||
wrapper.devMiddleware = devMiddleware; | ||
return wrapper; | ||
} | ||
wdm.koaWrapper = koaWrapper; | ||
module.exports = wdm; |
@@ -5,12 +5,13 @@ "use strict"; | ||
const mime = require("mime-types"); | ||
const onFinishedStream = require("on-finished"); | ||
const getFilenameFromUrl = require("./utils/getFilenameFromUrl"); | ||
const { | ||
getHeaderFromRequest, | ||
getHeaderFromResponse, | ||
setHeaderForResponse, | ||
setStatusCode, | ||
send, | ||
sendError | ||
pipe, | ||
createReadStreamOrReadFileSync | ||
} = require("./utils/compatibleAPI"); | ||
const ready = require("./utils/ready"); | ||
const parseTokenList = require("./utils/parseTokenList"); | ||
const memorize = require("./utils/memorize"); | ||
@@ -21,3 +22,6 @@ /** @typedef {import("./index.js").NextFunction} NextFunction */ | ||
/** @typedef {import("./index.js").NormalizedHeaders} NormalizedHeaders */ | ||
/** @typedef {import("fs").ReadStream} ReadStream */ | ||
const BYTES_RANGE_REGEXP = /^ *bytes/i; | ||
/** | ||
@@ -32,7 +36,79 @@ * @param {string} type | ||
} | ||
const BYTES_RANGE_REGEXP = /^ *bytes/i; | ||
/** | ||
* Parse an HTTP Date into a number. | ||
* | ||
* @param {string} date | ||
* @returns {number} | ||
*/ | ||
function parseHttpDate(date) { | ||
const timestamp = date && Date.parse(date); | ||
// istanbul ignore next: guard against date.js Date.parse patching | ||
return typeof timestamp === "number" ? timestamp : NaN; | ||
} | ||
const CACHE_CONTROL_NO_CACHE_REGEXP = /(?:^|,)\s*?no-cache\s*?(?:,|$)/; | ||
/** | ||
* @param {import("fs").ReadStream} stream stream | ||
* @param {boolean} suppress do need suppress? | ||
* @returns {void} | ||
*/ | ||
function destroyStream(stream, suppress) { | ||
if (typeof stream.destroy === "function") { | ||
stream.destroy(); | ||
} | ||
if (typeof stream.close === "function") { | ||
// Node.js core bug workaround | ||
stream.on("open", | ||
/** | ||
* @this {import("fs").ReadStream} | ||
*/ | ||
function onOpenClose() { | ||
// @ts-ignore | ||
if (typeof this.fd === "number") { | ||
// actually close down the fd | ||
this.close(); | ||
} | ||
}); | ||
} | ||
if (typeof stream.addListener === "function" && suppress) { | ||
stream.removeAllListeners("error"); | ||
stream.addListener("error", () => {}); | ||
} | ||
} | ||
/** @type {Record<number, string>} */ | ||
const statuses = { | ||
400: "Bad Request", | ||
403: "Forbidden", | ||
404: "Not Found", | ||
416: "Range Not Satisfiable", | ||
500: "Internal Server Error" | ||
}; | ||
const parseRangeHeaders = memorize( | ||
/** | ||
* @param {string} value | ||
* @returns {import("range-parser").Result | import("range-parser").Ranges} | ||
*/ | ||
value => { | ||
const [len, rangeHeader] = value.split("|"); | ||
// eslint-disable-next-line global-require | ||
return require("range-parser")(Number(len), rangeHeader, { | ||
combine: true | ||
}); | ||
}); | ||
/** | ||
* @template {IncomingMessage} Request | ||
* @template {ServerResponse} Response | ||
* @typedef {Object} SendErrorOptions send error options | ||
* @property {Record<string, number | string | string[] | undefined>=} headers headers | ||
* @property {import("./index").ModifyResponseData<Request, Response>=} modifyResponseData modify response data callback | ||
*/ | ||
/** | ||
* @template {IncomingMessage} Request | ||
* @template {ServerResponse} Response | ||
* @param {import("./index.js").FilledContext<Request, Response>} context | ||
@@ -48,7 +124,2 @@ * @return {import("./index.js").Middleware<Request, Response>} | ||
res.locals = res.locals || {}; | ||
if (req.method && !acceptedMethods.includes(req.method)) { | ||
await goNext(); | ||
return; | ||
} | ||
ready(context, processRequest, req); | ||
async function goNext() { | ||
@@ -69,3 +140,219 @@ if (!context.options.serverSideRender) { | ||
} | ||
if (req.method && !acceptedMethods.includes(req.method)) { | ||
await goNext(); | ||
return; | ||
} | ||
/** | ||
* @param {number} status status | ||
* @param {Partial<SendErrorOptions<Request, Response>>=} options options | ||
* @returns {void} | ||
*/ | ||
function sendError(status, options) { | ||
// eslint-disable-next-line global-require | ||
const escapeHtml = require("./utils/escapeHtml"); | ||
const content = statuses[status] || String(status); | ||
let document = Buffer.from(`<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8"> | ||
<title>Error</title> | ||
</head> | ||
<body> | ||
<pre>${escapeHtml(content)}</pre> | ||
</body> | ||
</html>`, "utf-8"); | ||
// Clear existing headers | ||
const headers = res.getHeaderNames(); | ||
for (let i = 0; i < headers.length; i++) { | ||
res.removeHeader(headers[i]); | ||
} | ||
if (options && options.headers) { | ||
const keys = Object.keys(options.headers); | ||
for (let i = 0; i < keys.length; i++) { | ||
const key = keys[i]; | ||
const value = options.headers[key]; | ||
if (typeof value !== "undefined") { | ||
res.setHeader(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"); | ||
let byteLength = Buffer.byteLength(document); | ||
if (options && options.modifyResponseData) { | ||
({ | ||
data: document, | ||
byteLength | ||
} = /** @type {{ data: Buffer, byteLength: number }} */ | ||
options.modifyResponseData(req, res, document, byteLength)); | ||
} | ||
res.setHeader("Content-Length", byteLength); | ||
res.end(document); | ||
} | ||
function isConditionalGET() { | ||
return req.headers["if-match"] || req.headers["if-unmodified-since"] || req.headers["if-none-match"] || req.headers["if-modified-since"]; | ||
} | ||
function isPreconditionFailure() { | ||
// if-match | ||
const ifMatch = req.headers["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 | ||
// be a more accurate replacement for the condition in | ||
// 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"); | ||
return !etag || ifMatch !== "*" && parseTokenList(ifMatch).every(match => match !== etag && match !== `W/${etag}` && `W/${match}` !== etag); | ||
} | ||
// if-unmodified-since | ||
const ifUnmodifiedSince = req.headers["if-unmodified-since"]; | ||
if (ifUnmodifiedSince) { | ||
const unmodifiedSince = parseHttpDate(ifUnmodifiedSince); | ||
// A recipient MUST ignore the If-Unmodified-Since header field if the | ||
// received field-value is not a valid HTTP-date. | ||
if (!isNaN(unmodifiedSince)) { | ||
const lastModified = parseHttpDate( /** @type {string} */res.getHeader("Last-Modified")); | ||
return isNaN(lastModified) || lastModified > unmodifiedSince; | ||
} | ||
} | ||
return false; | ||
} | ||
/** | ||
* @returns {boolean} is cachable | ||
*/ | ||
function isCachable() { | ||
return res.statusCode >= 200 && res.statusCode < 300 || res.statusCode === 304; | ||
} | ||
/** | ||
* @param {import("http").OutgoingHttpHeaders} resHeaders | ||
* @returns {boolean} | ||
*/ | ||
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"]; | ||
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"]; | ||
// unconditional request | ||
if (!noneMatch && !modifiedSince) { | ||
return false; | ||
} | ||
// if-none-match | ||
if (noneMatch && noneMatch !== "*") { | ||
if (!resHeaders.etag) { | ||
return false; | ||
} | ||
const matches = parseTokenList(noneMatch); | ||
let etagStale = true; | ||
for (let i = 0; i < matches.length; i++) { | ||
const match = matches[i]; | ||
if (match === resHeaders.etag || match === `W/${resHeaders.etag}` || `W/${match}` === resHeaders.etag) { | ||
etagStale = false; | ||
break; | ||
} | ||
} | ||
if (etagStale) { | ||
return false; | ||
} | ||
} | ||
// A recipient MUST ignore If-Modified-Since if the request contains an If-None-Match header field; | ||
// the condition in If-None-Match is considered to be a more accurate replacement for the condition in If-Modified-Since, | ||
// and the two are only combined for the sake of interoperating with older intermediaries that might not implement If-None-Match. | ||
if (noneMatch) { | ||
return true; | ||
} | ||
// if-modified-since | ||
if (modifiedSince) { | ||
const lastModified = resHeaders["last-modified"]; | ||
// A recipient MUST ignore the If-Modified-Since header field if the | ||
// received field-value is not a valid HTTP-date, or if the request | ||
// method is neither GET nor HEAD. | ||
const modifiedStale = !lastModified || !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince)); | ||
if (modifiedStale) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
function isRangeFresh() { | ||
const ifRange = /** @type {string | undefined} */ | ||
req.headers["if-range"]; | ||
if (!ifRange) { | ||
return true; | ||
} | ||
// if-range as etag | ||
if (ifRange.indexOf('"') !== -1) { | ||
const etag = /** @type {string | undefined} */res.getHeader("ETag"); | ||
if (!etag) { | ||
return true; | ||
} | ||
return Boolean(etag && ifRange.indexOf(etag) !== -1); | ||
} | ||
// if-range as modified date | ||
const lastModified = /** @type {string | undefined} */ | ||
res.getHeader("Last-Modified"); | ||
if (!lastModified) { | ||
return true; | ||
} | ||
return parseHttpDate(lastModified) <= parseHttpDate(ifRange); | ||
} | ||
/** | ||
* @returns {string | undefined} | ||
*/ | ||
function getRangeHeader() { | ||
const rage = req.headers.range; | ||
if (rage && BYTES_RANGE_REGEXP.test(rage)) { | ||
return rage; | ||
} | ||
// eslint-disable-next-line no-undefined | ||
return undefined; | ||
} | ||
/** | ||
* @param {import("range-parser").Range} range | ||
* @returns {[number, number]} | ||
*/ | ||
function getOffsetAndLenFromRange(range) { | ||
const offset = range.start; | ||
const len = range.end - range.start + 1; | ||
return [offset, len]; | ||
} | ||
/** | ||
* @param {number} offset | ||
* @param {number} len | ||
* @returns {[number, number]} | ||
*/ | ||
function calcStartAndEnd(offset, len) { | ||
const start = offset; | ||
const end = Math.max(offset, offset + len - 1); | ||
return [start, end]; | ||
} | ||
async function processRequest() { | ||
// Pipe and SendFile | ||
/** @type {import("./utils/getFilenameFromUrl").Extra} */ | ||
@@ -78,3 +365,3 @@ const extra = {}; | ||
} | ||
sendError(req, res, extra.errorCode, { | ||
sendError(extra.errorCode, { | ||
modifyResponseData: context.options.modifyResponseData | ||
@@ -88,2 +375,9 @@ }); | ||
} | ||
const { | ||
size | ||
} = /** @type {import("fs").Stats} */extra.stats; | ||
let len = size; | ||
let offset = 0; | ||
// Send logic | ||
let { | ||
@@ -112,6 +406,6 @@ headers | ||
headers.forEach(header => { | ||
setHeaderForResponse(res, header.key, header.value); | ||
res.setHeader(header.key, header.value); | ||
}); | ||
} | ||
if (!getHeaderFromResponse(res, "Content-Type")) { | ||
if (!res.getHeader("Content-Type")) { | ||
// content-type name(like application/javascript; charset=utf-8) or false | ||
@@ -123,23 +417,107 @@ const contentType = mime.contentType(path.extname(filename)); | ||
if (contentType) { | ||
setHeaderForResponse(res, "Content-Type", contentType); | ||
res.setHeader("Content-Type", contentType); | ||
} else if (context.options.mimeTypeDefault) { | ||
setHeaderForResponse(res, "Content-Type", context.options.mimeTypeDefault); | ||
res.setHeader("Content-Type", context.options.mimeTypeDefault); | ||
} | ||
} | ||
if (!getHeaderFromResponse(res, "Accept-Ranges")) { | ||
setHeaderForResponse(res, "Accept-Ranges", "bytes"); | ||
if (!res.getHeader("Accept-Ranges")) { | ||
res.setHeader("Accept-Ranges", "bytes"); | ||
} | ||
const rangeHeader = /** @type {string} */ | ||
getHeaderFromRequest(req, "range"); | ||
let len = /** @type {import("fs").Stats} */extra.stats.size; | ||
let offset = 0; | ||
if (rangeHeader && BYTES_RANGE_REGEXP.test(rangeHeader)) { | ||
// eslint-disable-next-line global-require | ||
const parsedRanges = require("range-parser")(len, rangeHeader, { | ||
combine: true | ||
}); | ||
if (context.options.lastModified && !res.getHeader("Last-Modified")) { | ||
const modified = /** @type {import("fs").Stats} */ | ||
extra.stats.mtime.toUTCString(); | ||
res.setHeader("Last-Modified", modified); | ||
} | ||
/** @type {number} */ | ||
let start; | ||
/** @type {number} */ | ||
let end; | ||
/** @type {undefined | Buffer | ReadStream} */ | ||
let bufferOrStream; | ||
/** @type {number} */ | ||
let byteLength; | ||
const rangeHeader = getRangeHeader(); | ||
if (context.options.etag && !res.getHeader("ETag")) { | ||
/** @type {import("fs").Stats | Buffer | ReadStream | undefined} */ | ||
let value; | ||
// TODO cache etag generation? | ||
if (context.options.etag === "weak") { | ||
value = /** @type {import("fs").Stats} */extra.stats; | ||
} else { | ||
if (rangeHeader) { | ||
const parsedRanges = /** @type {import("range-parser").Ranges | import("range-parser").Result} */ | ||
parseRangeHeaders(`${size}|${rangeHeader}`); | ||
if (parsedRanges !== -2 && parsedRanges !== -1 && parsedRanges.length === 1) { | ||
[offset, len] = getOffsetAndLenFromRange(parsedRanges[0]); | ||
} | ||
} | ||
[start, end] = calcStartAndEnd(offset, len); | ||
try { | ||
const result = createReadStreamOrReadFileSync(filename, context.outputFileSystem, start, end); | ||
value = result.bufferOrStream; | ||
({ | ||
bufferOrStream, | ||
byteLength | ||
} = result); | ||
} catch (_err) { | ||
// Ignore here | ||
} | ||
} | ||
if (value) { | ||
// eslint-disable-next-line global-require | ||
const result = await require("./utils/etag")(value); | ||
// Because we already read stream, we can cache buffer to avoid extra read from fs | ||
if (result.buffer) { | ||
bufferOrStream = result.buffer; | ||
} | ||
res.setHeader("ETag", result.hash); | ||
} | ||
} | ||
// Conditional GET support | ||
if (isConditionalGET()) { | ||
if (isPreconditionFailure()) { | ||
sendError(412, { | ||
modifyResponseData: context.options.modifyResponseData | ||
}); | ||
return; | ||
} | ||
// For Koa | ||
if (res.statusCode === 404) { | ||
setStatusCode(res, 200); | ||
} | ||
if (isCachable() && isFresh({ | ||
etag: ( /** @type {string | undefined} */res.getHeader("ETag")), | ||
"last-modified": ( /** @type {string | undefined} */ | ||
res.getHeader("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(); | ||
return; | ||
} | ||
} | ||
if (rangeHeader) { | ||
let parsedRanges = /** @type {import("range-parser").Ranges | import("range-parser").Result | []} */ | ||
parseRangeHeaders(`${size}|${rangeHeader}`); | ||
// If-Range support | ||
if (!isRangeFresh()) { | ||
parsedRanges = []; | ||
} | ||
if (parsedRanges === -1) { | ||
context.logger.error("Unsatisfiable range for 'Range' header."); | ||
setHeaderForResponse(res, "Content-Range", getValueContentRangeHeader("bytes", len)); | ||
sendError(req, res, 416, { | ||
res.setHeader("Content-Range", getValueContentRangeHeader("bytes", size)); | ||
sendError(416, { | ||
headers: { | ||
@@ -159,16 +537,80 @@ "Content-Range": res.getHeader("Content-Range") | ||
setStatusCode(res, 206); | ||
setHeaderForResponse(res, "Content-Range", getValueContentRangeHeader("bytes", len, /** @type {import("range-parser").Ranges} */parsedRanges[0])); | ||
offset += parsedRanges[0].start; | ||
len = parsedRanges[0].end - parsedRanges[0].start + 1; | ||
res.setHeader("Content-Range", getValueContentRangeHeader("bytes", size, /** @type {import("range-parser").Ranges} */parsedRanges[0])); | ||
[offset, len] = getOffsetAndLenFromRange(parsedRanges[0]); | ||
} | ||
} | ||
const start = offset; | ||
const end = Math.max(offset, offset + len - 1); | ||
send(req, res, filename, start, end, goNext, { | ||
modifyResponseData: context.options.modifyResponseData, | ||
outputFileSystem: context.outputFileSystem | ||
// When strong Etag generation is enabled we already read file, so we can skip extra fs call | ||
if (!bufferOrStream) { | ||
[start, end] = calcStartAndEnd(offset, len); | ||
try { | ||
({ | ||
bufferOrStream, | ||
byteLength | ||
} = createReadStreamOrReadFileSync(filename, context.outputFileSystem, start, end)); | ||
} catch (_ignoreError) { | ||
await goNext(); | ||
return; | ||
} | ||
} | ||
if (context.options.modifyResponseData) { | ||
({ | ||
data: bufferOrStream, | ||
byteLength | ||
} = context.options.modifyResponseData(req, res, bufferOrStream, | ||
// @ts-ignore | ||
byteLength)); | ||
} | ||
// @ts-ignore | ||
res.setHeader("Content-Length", byteLength); | ||
if (req.method === "HEAD") { | ||
// For Koa | ||
if (res.statusCode === 404) { | ||
setStatusCode(res, 200); | ||
} | ||
res.end(); | ||
return; | ||
} | ||
const isPipeSupports = typeof ( /** @type {import("fs").ReadStream} */bufferOrStream.pipe) === "function"; | ||
if (!isPipeSupports) { | ||
send(res, /** @type {Buffer} */bufferOrStream); | ||
return; | ||
} | ||
// Cleanup | ||
const cleanup = () => { | ||
destroyStream( /** @type {import("fs").ReadStream} */bufferOrStream, true); | ||
}; | ||
// 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; | ||
} | ||
}); | ||
pipe(res, /** @type {ReadStream} */bufferOrStream); | ||
// Response finished, cleanup | ||
onFinishedStream(res, cleanup); | ||
} | ||
ready(context, processRequest, req); | ||
}; | ||
} | ||
module.exports = wrapper; |
@@ -132,2 +132,12 @@ { | ||
"instanceof": "Function" | ||
}, | ||
"etag": { | ||
"description": "Enable or disable etag generation.", | ||
"link": "https://github.com/webpack/webpack-dev-middleware#etag", | ||
"enum": ["weak", "strong"] | ||
}, | ||
"lastModified": { | ||
"description": "Enable or disable `Last-Modified` header. Uses the file system's last modified value.", | ||
"link": "https://github.com/webpack/webpack-dev-middleware#lastmodified", | ||
"type": "boolean" | ||
} | ||
@@ -134,0 +144,0 @@ }, |
"use strict"; | ||
const onFinishedStream = require("on-finished"); | ||
const escapeHtml = require("./escapeHtml"); | ||
/** @typedef {import("../index.js").IncomingMessage} IncomingMessage */ | ||
/** @typedef {import("../index.js").ServerResponse} ServerResponse */ | ||
/** @typedef {import("fs").ReadStream} ReadStream */ | ||
/** | ||
* @typedef {Object} ExpectedRequest | ||
* @property {(name: string) => string | undefined} get | ||
*/ | ||
/** | ||
* @typedef {Object} ExpectedResponse | ||
* @property {(name: string) => string | string[] | undefined} get | ||
* @property {(name: string, value: number | string | string[]) => void} set | ||
* @property {(status: number) => void} status | ||
* @property {(data: any) => void} send | ||
* @property {(status: number) => void} [status] | ||
* @property {(data: any) => void} [send] | ||
* @property {(data: any) => void} [pipeInto] | ||
*/ | ||
/** | ||
* @template {ServerResponse} Response | ||
* @template {ServerResponse & ExpectedResponse} Response | ||
* @param {Response} res | ||
* @returns {string[]} | ||
*/ | ||
function getHeaderNames(res) { | ||
if (typeof res.getHeaderNames !== "function") { | ||
// @ts-ignore | ||
// eslint-disable-next-line no-underscore-dangle | ||
return Object.keys(res._headers || {}); | ||
} | ||
return res.getHeaderNames(); | ||
} | ||
/** | ||
* @template {IncomingMessage} Request | ||
* @param {Request} req | ||
* @param {string} name | ||
* @returns {string | string[] | undefined} | ||
*/ | ||
function getHeaderFromRequest(req, name) { | ||
// Express API | ||
if (typeof ( /** @type {Request & ExpectedRequest} */req.get) === "function") { | ||
return /** @type {Request & ExpectedRequest} */req.get(name); | ||
} | ||
// Node.js API | ||
return req.headers[name]; | ||
} | ||
/** | ||
* @template {ServerResponse} Response | ||
* @param {Response} res | ||
* @param {string} name | ||
* @returns {number | string | string[] | undefined} | ||
*/ | ||
function getHeaderFromResponse(res, name) { | ||
// Express API | ||
if (typeof ( /** @type {Response & ExpectedResponse} */res.get) === "function") { | ||
return /** @type {Response & ExpectedResponse} */res.get(name); | ||
} | ||
// Node.js API | ||
return res.getHeader(name); | ||
} | ||
/** | ||
* @template {ServerResponse} Response | ||
* @param {Response} res | ||
* @param {string} name | ||
* @param {number | string | string[]} value | ||
* @returns {void} | ||
*/ | ||
function setHeaderForResponse(res, name, value) { | ||
// Express API | ||
if (typeof ( /** @type {Response & ExpectedResponse} */res.set) === "function") { | ||
/** @type {Response & ExpectedResponse} */ | ||
res.set(name, typeof value === "number" ? String(value) : value); | ||
return; | ||
} | ||
// Node.js API | ||
res.setHeader(name, value); | ||
} | ||
/** | ||
* @template {ServerResponse} Response | ||
* @param {Response} res | ||
* @param {Record<string, number | string | string[] | undefined>} headers | ||
*/ | ||
function setHeadersForResponse(res, headers) { | ||
const keys = Object.keys(headers); | ||
for (let i = 0; i < keys.length; i++) { | ||
const key = keys[i]; | ||
const value = headers[key]; | ||
if (typeof value !== "undefined") { | ||
setHeaderForResponse(res, key, value); | ||
} | ||
} | ||
} | ||
/** | ||
* @template {ServerResponse} Response | ||
* @param {Response} res | ||
*/ | ||
function clearHeadersForResponse(res) { | ||
const headers = getHeaderNames(res); | ||
for (let i = 0; i < headers.length; i++) { | ||
res.removeHeader(headers[i]); | ||
} | ||
} | ||
/** | ||
* @template {ServerResponse} Response | ||
* @param {Response} res | ||
* @param {number} code | ||
*/ | ||
function setStatusCode(res, code) { | ||
if (typeof ( /** @type {Response & ExpectedResponse} */res.status) === "function") { | ||
/** @type {Response & ExpectedResponse} */ | ||
// Pseudo API | ||
if (typeof res.status === "function") { | ||
res.status(code); | ||
@@ -127,2 +25,3 @@ return; | ||
// Node.js API | ||
// eslint-disable-next-line no-param-reassign | ||
@@ -133,198 +32,74 @@ res.statusCode = code; | ||
/** | ||
* @param {import("fs").ReadStream} stream stream | ||
* @param {boolean} suppress do need suppress? | ||
* @returns {void} | ||
* @template {ServerResponse} Response | ||
* @param {Response & ExpectedResponse} res | ||
* @param {import("fs").ReadStream} bufferOrStream | ||
*/ | ||
function destroyStream(stream, suppress) { | ||
if (typeof stream.destroy === "function") { | ||
stream.destroy(); | ||
function pipe(res, bufferOrStream) { | ||
// Pseudo API and Koa API | ||
if (typeof ( /** @type {Response & ExpectedResponse} */res.pipeInto) === "function") { | ||
// Writable stream into Readable stream | ||
res.pipeInto(bufferOrStream); | ||
return; | ||
} | ||
if (typeof stream.close === "function") { | ||
// Node.js core bug workaround | ||
stream.on("open", | ||
/** | ||
* @this {import("fs").ReadStream} | ||
*/ | ||
function onOpenClose() { | ||
// @ts-ignore | ||
if (typeof this.fd === "number") { | ||
// actually close down the fd | ||
this.close(); | ||
} | ||
}); | ||
} | ||
if (typeof stream.addListener === "function" && suppress) { | ||
stream.removeAllListeners("error"); | ||
stream.addListener("error", () => {}); | ||
} | ||
// Node.js API and Express API and Hapi API | ||
bufferOrStream.pipe(res); | ||
} | ||
/** @type {Record<number, string>} */ | ||
const statuses = { | ||
400: "Bad Request", | ||
403: "Forbidden", | ||
404: "Not Found", | ||
416: "Range Not Satisfiable", | ||
500: "Internal Server Error" | ||
}; | ||
/** | ||
* @template {IncomingMessage} Request | ||
* @template {ServerResponse} Response | ||
* @param {Request} req response | ||
* @param {Response} res response | ||
* @param {number} status status | ||
* @param {Partial<SendOptions<Request, Response>>=} options options | ||
* @returns {void} | ||
* @param {Response & ExpectedResponse} res | ||
* @param {string | Buffer} bufferOrStream | ||
*/ | ||
function sendError(req, res, status, options) { | ||
const content = statuses[status] || String(status); | ||
let document = `<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8"> | ||
<title>Error</title> | ||
</head> | ||
<body> | ||
<pre>${escapeHtml(content)}</pre> | ||
</body> | ||
</html>`; | ||
// Clear existing headers | ||
clearHeadersForResponse(res); | ||
if (options && options.headers) { | ||
setHeadersForResponse(res, options.headers); | ||
function send(res, bufferOrStream) { | ||
// Pseudo API and Express API and Koa API | ||
if (typeof res.send === "function") { | ||
res.send(bufferOrStream); | ||
return; | ||
} | ||
// Send basic response | ||
setStatusCode(res, status); | ||
setHeaderForResponse(res, "Content-Type", "text/html; charset=utf-8"); | ||
setHeaderForResponse(res, "Content-Security-Policy", "default-src 'none'"); | ||
setHeaderForResponse(res, "X-Content-Type-Options", "nosniff"); | ||
let byteLength = Buffer.byteLength(document); | ||
if (options && options.modifyResponseData) { | ||
({ | ||
data: document, | ||
byteLength | ||
} = /** @type {{data: string, byteLength: number }} */ | ||
options.modifyResponseData(req, res, document, byteLength)); | ||
} | ||
setHeaderForResponse(res, "Content-Length", byteLength); | ||
res.end(document); | ||
res.end(bufferOrStream); | ||
} | ||
/** | ||
* @template {IncomingMessage} Request | ||
* @template {ServerResponse} Response | ||
* @typedef {Object} SendOptions send error options | ||
* @property {Record<string, number | string | string[] | undefined>=} headers headers | ||
* @property {import("../index").ModifyResponseData<Request, Response>=} modifyResponseData modify response data callback | ||
* @property {import("../index").OutputFileSystem} outputFileSystem modify response data callback | ||
*/ | ||
/** | ||
* @template {IncomingMessage} Request | ||
* @template {ServerResponse} Response | ||
* @param {Request} req | ||
* @param {Response} res | ||
* @param {string} filename | ||
* @param {import("../index").OutputFileSystem} outputFileSystem | ||
* @param {number} start | ||
* @param {number} end | ||
* @param {() => Promise<void>} goNext | ||
* @param {SendOptions<Request, Response>} options | ||
* @returns {{ bufferOrStream: (Buffer | import("fs").ReadStream), byteLength: number }} | ||
*/ | ||
async function send(req, res, filename, start, end, goNext, options) { | ||
const isFsSupportsStream = typeof options.outputFileSystem.createReadStream === "function"; | ||
/** @type {string | Buffer | ReadStream} */ | ||
function createReadStreamOrReadFileSync(filename, outputFileSystem, start, end) { | ||
/** @type {string | Buffer | import("fs").ReadStream} */ | ||
let bufferOrStream; | ||
/** @type {number} */ | ||
let byteLength; | ||
try { | ||
if (isFsSupportsStream) { | ||
bufferOrStream = /** @type {import("fs").createReadStream} */ | ||
options.outputFileSystem.createReadStream(filename, { | ||
start, | ||
end | ||
}); | ||
// Handle files with zero bytes | ||
byteLength = end === 0 ? 0 : end - start + 1; | ||
} else { | ||
bufferOrStream = /** @type {import("fs").readFileSync} */options.outputFileSystem.readFileSync(filename); | ||
({ | ||
byteLength | ||
} = bufferOrStream); | ||
} | ||
} catch (_ignoreError) { | ||
await goNext(); | ||
return; | ||
} | ||
if (options.modifyResponseData) { | ||
({ | ||
data: bufferOrStream, | ||
byteLength | ||
} = options.modifyResponseData(req, res, bufferOrStream, byteLength)); | ||
} | ||
if (typeof ( /** @type {import("fs").ReadStream} */bufferOrStream.pipe) === "function") { | ||
setHeaderForResponse(res, "Content-Length", byteLength); | ||
if (req.method === "HEAD") { | ||
res.end(); | ||
return; | ||
} | ||
/** @type {import("fs").ReadStream} */ | ||
bufferOrStream.pipe(res); | ||
// Cleanup | ||
const cleanup = () => { | ||
destroyStream( /** @type {import("fs").ReadStream} */bufferOrStream, true); | ||
}; | ||
// Response finished, cleanup | ||
onFinishedStream(res, cleanup); | ||
// 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(req, res, 404, options); | ||
break; | ||
default: | ||
sendError(req, res, 500, options); | ||
break; | ||
} | ||
// Stream logic | ||
const isFsSupportsStream = typeof outputFileSystem.createReadStream === "function"; | ||
if (isFsSupportsStream) { | ||
bufferOrStream = /** @type {import("fs").createReadStream} */ | ||
outputFileSystem.createReadStream(filename, { | ||
start, | ||
end | ||
}); | ||
return; | ||
} | ||
// Express API | ||
if (typeof ( /** @type {Response & ExpectedResponse} */res.send) === "function") { | ||
/** @type {Response & ExpectedResponse} */ | ||
res.send(bufferOrStream); | ||
return; | ||
} | ||
// Only Node.js API used | ||
res.setHeader("Content-Length", byteLength); | ||
if (req.method === "HEAD") { | ||
res.end(); | ||
// Handle files with zero bytes | ||
byteLength = end === 0 ? 0 : end - start + 1; | ||
} else { | ||
res.end(bufferOrStream); | ||
bufferOrStream = /** @type {import("fs").readFileSync} */ | ||
outputFileSystem.readFileSync(filename); | ||
({ | ||
byteLength | ||
} = bufferOrStream); | ||
} | ||
return { | ||
bufferOrStream, | ||
byteLength | ||
}; | ||
} | ||
module.exports = { | ||
getHeaderNames, | ||
getHeaderFromRequest, | ||
getHeaderFromResponse, | ||
setHeaderForResponse, | ||
setStatusCode, | ||
send, | ||
sendError | ||
pipe, | ||
createReadStreamOrReadFileSync | ||
}; |
@@ -9,2 +9,3 @@ "use strict"; | ||
const getPaths = require("./getPaths"); | ||
const memorize = require("./memorize"); | ||
@@ -14,36 +15,4 @@ /** @typedef {import("../index.js").IncomingMessage} IncomingMessage */ | ||
const cacheStore = new WeakMap(); | ||
/** | ||
* @template T | ||
* @param {Function} fn | ||
* @param {{ cache?: Map<string, { data: T }> } | undefined} cache | ||
* @param {(value: T) => T} callback | ||
* @returns {any} | ||
*/ | ||
const mem = (fn, { | ||
cache = new Map() | ||
} = {}, callback) => { | ||
/** | ||
* @param {any} arguments_ | ||
* @return {any} | ||
*/ | ||
const memoized = (...arguments_) => { | ||
const [key] = arguments_; | ||
const cacheItem = cache.get(key); | ||
if (cacheItem) { | ||
return cacheItem.data; | ||
} | ||
let result = fn.apply(void 0, arguments_); | ||
result = callback(result); | ||
cache.set(key, { | ||
data: result | ||
}); | ||
return result; | ||
}; | ||
cacheStore.set(memoized, cache); | ||
return memoized; | ||
}; | ||
// eslint-disable-next-line no-undefined | ||
const memoizedParse = mem(parse, undefined, value => { | ||
const memoizedParse = memorize(parse, undefined, value => { | ||
if (value.pathname) { | ||
@@ -77,2 +46,3 @@ // eslint-disable-next-line no-param-reassign | ||
// TODO refactor me in the next major release, this function should return `{ filename, stats, error }` | ||
// TODO fix redirect logic when `/` at the end, like https://github.com/pillarjs/send/blob/master/index.js#L586 | ||
/** | ||
@@ -79,0 +49,0 @@ * @template {IncomingMessage} Request |
{ | ||
"name": "webpack-dev-middleware", | ||
"version": "7.1.1", | ||
"version": "7.2.0", | ||
"description": "A development middleware for webpack", | ||
@@ -69,2 +69,4 @@ "license": "MIT", | ||
"@commitlint/config-conventional": "^19.0.3", | ||
"@fastify/express": "^2.3.0", | ||
"@hapi/hapi": "^21.3.7", | ||
"@types/connect": "^3.4.35", | ||
@@ -89,5 +91,8 @@ "@types/express": "^4.17.13", | ||
"express": "^4.17.1", | ||
"fastify": "^4.26.2", | ||
"file-loader": "^6.2.0", | ||
"husky": "^9.0.10", | ||
"jest": "^29.3.1", | ||
"joi": "^17.12.2", | ||
"koa": "^2.15.2", | ||
"lint-staged": "^15.2.0", | ||
@@ -94,0 +99,0 @@ "npm-run-all": "^4.1.5", |
141
README.md
@@ -63,15 +63,16 @@ <div align="center"> | ||
| Name | Type | Default | Description | | ||
| :---------------------------------------------: | :-----------------------: | :-------------------------------------------: | :------------------------------------------------------------------------------------------------------------------- | | ||
| **[`methods`](#methods)** | `Array` | `[ 'GET', 'HEAD' ]` | Allows to pass the list of HTTP request methods accepted by the middleware | | ||
| **[`headers`](#headers)** | `Array\|Object\|Function` | `undefined` | Allows to pass custom HTTP headers on each request. | | ||
| **[`index`](#index)** | `Boolean\|String` | `index.html` | If `false` (but not `undefined`), the server will not respond to requests to the root URL. | | ||
| **[`mimeTypes`](#mimetypes)** | `Object` | `undefined` | Allows to register custom mime types or extension mappings. | | ||
| **[`mimeTypeDefault`](#mimetypedefault)** | `String` | `undefined` | Allows to register a default mime type when we can't determine the content type. | | ||
| **[`publicPath`](#publicpath)** | `String` | `output.publicPath` (from a configuration) | The public path that the middleware is bound to. | | ||
| **[`stats`](#stats)** | `Boolean\|String\|Object` | `stats` (from a configuration) | Stats options object or preset name. | | ||
| **[`serverSideRender`](#serversiderender)** | `Boolean` | `undefined` | Instructs the module to enable or disable the server-side rendering mode. | | ||
| **[`writeToDisk`](#writetodisk)** | `Boolean\|Function` | `false` | Instructs the module to write files to the configured location on disk as specified in your `webpack` configuration. | | ||
| **[`outputFileSystem`](#outputfilesystem)** | `Object` | [`memfs`](https://github.com/streamich/memfs) | Set the default file system which will be used by webpack as primary destination of generated files. | | ||
| **[`modifyResponseData`](#modifyresponsedata)** | `Function` | `undefined` | Allows to set up a callback to change the response data. | | ||
| Name | Type | Default | Description | | ||
| :---------------------------------------------: | :---------------------------: | :-------------------------------------------: | :--------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | | ||
| **[`methods`](#methods)** | `Array` | `[ 'GET', 'HEAD' ]` | Allows to pass the list of HTTP request methods accepted by the middleware | | ||
| **[`headers`](#headers)** | `Array\ | Object\ | Function` | `undefined` | Allows to pass custom HTTP headers on each request. | | ||
| **[`index`](#index)** | `Boolean\ | String` | `index.html` | If `false` (but not `undefined`), the server will not respond to requests to the root URL. | | ||
| **[`mimeTypes`](#mimetypes)** | `Object` | `undefined` | Allows to register custom mime types or extension mappings. | | ||
| **[`mimeTypeDefault`](#mimetypedefault)** | `String` | `undefined` | Allows to register a default mime type when we can't determine the content type. | | ||
| **[`etag`](#tag)** | `boolean\| "weak"\| "strong"` | `undefined` | Enable or disable etag generation. | | ||
| **[`publicPath`](#publicpath)** | `String` | `output.publicPath` (from a configuration) | The public path that the middleware is bound to. | | ||
| **[`stats`](#stats)** | `Boolean\ | String\ | Object` | `stats` (from a configuration) | Stats options object or preset name. | | ||
| **[`serverSideRender`](#serversiderender)** | `Boolean` | `undefined` | Instructs the module to enable or disable the server-side rendering mode. | | ||
| **[`writeToDisk`](#writetodisk)** | `Boolean\ | Function` | `false` | Instructs the module to write files to the configured location on disk as specified in your `webpack` configuration. | | ||
| **[`outputFileSystem`](#outputfilesystem)** | `Object` | [`memfs`](https://github.com/streamich/memfs) | Set the default file system which will be used by webpack as primary destination of generated files. | | ||
| **[`modifyResponseData`](#modifyresponsedata)** | `Function` | `undefined` | Allows to set up a callback to change the response data. | | ||
@@ -175,2 +176,16 @@ The middleware accepts an `options` Object. The following is a property reference for the Object. | ||
### etag | ||
Type: `"weak" | "strong"` | ||
Default: `undefined` | ||
Enable or disable etag generation. Boolean value use | ||
### lastModified | ||
Type: `Boolean` | ||
Default: `undefined` | ||
Enable or disable `Last-Modified` header. Uses the file system's last modified value. | ||
### publicPath | ||
@@ -545,2 +560,94 @@ | ||
### Connect | ||
```js | ||
const connect = require("connect"); | ||
const http = require("http"); | ||
const webpack = require("webpack"); | ||
const webpackConfig = require("./webpack.config.js"); | ||
const devMiddleware = require("webpack-dev-middleware"); | ||
const compiler = webpack(webpackConfig); | ||
const devMiddlewareOptions = { | ||
/** Your webpack-dev-middleware-options */ | ||
}; | ||
const app = connect(); | ||
app.use(devMiddleware(compiler, devMiddlewareOptions)); | ||
http.createServer(app).listen(3000); | ||
``` | ||
### Express | ||
```js | ||
const express = require("express"); | ||
const webpack = require("webpack"); | ||
const webpackConfig = require("./webpack.config.js"); | ||
const devMiddleware = require("webpack-dev-middleware"); | ||
const compiler = webpack(webpackConfig); | ||
const devMiddlewareOptions = { | ||
/** Your webpack-dev-middleware-options */ | ||
}; | ||
const app = express(); | ||
app.use(devMiddleware(compiler, devMiddlewareOptions)); | ||
app.listen(3000, () => console.log("Example app listening on port 3000!")); | ||
``` | ||
### Koa | ||
```js | ||
const Koa = require("koa"); | ||
const webpack = require("webpack"); | ||
const webpackConfig = require("./test/fixtures/webpack.simple.config"); | ||
const middleware = require("./dist"); | ||
const compiler = webpack(webpackConfig); | ||
const devMiddlewareOptions = { | ||
/** Your webpack-dev-middleware-options */ | ||
}; | ||
const app = new Koa(); | ||
app.use(middleware.koaWrapper(compiler, devMiddlewareOptions)); | ||
app.listen(3000); | ||
``` | ||
### Hapi | ||
```js | ||
const Hapi = require("@hapi/hapi"); | ||
const webpack = require("webpack"); | ||
const webpackConfig = require("./webpack.config.js"); | ||
const devMiddleware = require("webpack-dev-middleware"); | ||
const compiler = webpack(webpackConfig); | ||
const devMiddlewareOptions = {}; | ||
(async () => { | ||
const server = Hapi.server({ port: 3000, host: "localhost" }); | ||
await server.register({ | ||
plugin: devMiddleware.hapiPlugin(), | ||
options: { | ||
// The `compiler` option is required | ||
compiler, | ||
...devMiddlewareOptions, | ||
}, | ||
}); | ||
await server.start(); | ||
console.log("Server running on %s", server.info.uri); | ||
})(); | ||
process.on("unhandledRejection", (err) => { | ||
console.log(err); | ||
process.exit(1); | ||
}); | ||
``` | ||
### Fastify | ||
@@ -557,7 +664,9 @@ | ||
const compiler = webpack(webpackConfig); | ||
const { publicPath } = webpackConfig.output; | ||
const devMiddlewareOptions = { | ||
/** Your webpack-dev-middleware-options */ | ||
}; | ||
(async () => { | ||
await fastify.register(require("fastify-express")); | ||
await fastify.use(devMiddleware(compiler, { publicPath })); | ||
await fastify.register(require("@fastify/express")); | ||
await fastify.use(devMiddleware(compiler, devMiddlewareOptions)); | ||
await fastify.listen(3000); | ||
@@ -564,0 +673,0 @@ })(); |
@@ -41,12 +41,12 @@ /// <reference types="node" /> | ||
* @typedef {Object} ResponseData | ||
* @property {string | Buffer | ReadStream} data | ||
* @property {Buffer | ReadStream} data | ||
* @property {number} byteLength | ||
*/ | ||
/** | ||
* @template {IncomingMessage} RequestInternal | ||
* @template {ServerResponse} ResponseInternal | ||
* @template {IncomingMessage} [RequestInternal=IncomingMessage] | ||
* @template {ServerResponse} [ResponseInternal=ServerResponse] | ||
* @callback ModifyResponseData | ||
* @param {RequestInternal} req | ||
* @param {ResponseInternal} res | ||
* @param {string | Buffer | ReadStream} data | ||
* @param {Buffer | ReadStream} data | ||
* @param {number} byteLength | ||
@@ -56,4 +56,4 @@ * @return {ResponseData} | ||
/** | ||
* @template {IncomingMessage} RequestInternal | ||
* @template {ServerResponse} ResponseInternal | ||
* @template {IncomingMessage} [RequestInternal=IncomingMessage] | ||
* @template {ServerResponse} [ResponseInternal=ServerResponse] | ||
* @typedef {Object} Context | ||
@@ -70,4 +70,4 @@ * @property {boolean} state | ||
/** | ||
* @template {IncomingMessage} RequestInternal | ||
* @template {ServerResponse} ResponseInternal | ||
* @template {IncomingMessage} [RequestInternal=IncomingMessage] | ||
* @template {ServerResponse} [ResponseInternal=ServerResponse] | ||
* @typedef {WithoutUndefined<Context<RequestInternal, ResponseInternal>, "watching">} FilledContext | ||
@@ -77,9 +77,9 @@ */ | ||
/** | ||
* @template {IncomingMessage} RequestInternal | ||
* @template {ServerResponse} ResponseInternal | ||
* @template {IncomingMessage} [RequestInternal=IncomingMessage] | ||
* @template {ServerResponse} [ResponseInternal=ServerResponse] | ||
* @typedef {NormalizedHeaders | ((req: RequestInternal, res: ResponseInternal, context: Context<RequestInternal, ResponseInternal>) => void | undefined | NormalizedHeaders) | undefined} Headers | ||
*/ | ||
/** | ||
* @template {IncomingMessage} RequestInternal | ||
* @template {ServerResponse} ResponseInternal | ||
* @template {IncomingMessage} [RequestInternal = IncomingMessage] | ||
* @template {ServerResponse} [ResponseInternal = ServerResponse] | ||
* @typedef {Object} Options | ||
@@ -97,6 +97,8 @@ * @property {{[key: string]: string}} [mimeTypes] | ||
* @property {ModifyResponseData<RequestInternal, ResponseInternal>} [modifyResponseData] | ||
* @property {"weak" | "strong"} [etag] | ||
* @property {boolean} [lastModified] | ||
*/ | ||
/** | ||
* @template {IncomingMessage} RequestInternal | ||
* @template {ServerResponse} ResponseInternal | ||
* @template {IncomingMessage} [RequestInternal=IncomingMessage] | ||
* @template {ServerResponse} [ResponseInternal=ServerResponse] | ||
* @callback Middleware | ||
@@ -138,4 +140,4 @@ * @param {RequestInternal} req | ||
/** | ||
* @template {IncomingMessage} RequestInternal | ||
* @template {ServerResponse} ResponseInternal | ||
* @template {IncomingMessage} [RequestInternal=IncomingMessage] | ||
* @template {ServerResponse} [ResponseInternal=ServerResponse] | ||
* @typedef {Middleware<RequestInternal, ResponseInternal> & AdditionalMethods<RequestInternal, ResponseInternal>} API | ||
@@ -154,4 +156,4 @@ */ | ||
/** | ||
* @template {IncomingMessage} RequestInternal | ||
* @template {ServerResponse} ResponseInternal | ||
* @template {IncomingMessage} [RequestInternal=IncomingMessage] | ||
* @template {ServerResponse} [ResponseInternal=ServerResponse] | ||
* @param {Compiler | MultiCompiler} compiler | ||
@@ -162,4 +164,5 @@ * @param {Options<RequestInternal, ResponseInternal>} [options] | ||
declare function wdm< | ||
RequestInternal extends import("http").IncomingMessage, | ||
ResponseInternal extends ServerResponse, | ||
RequestInternal extends | ||
import("http").IncomingMessage = import("http").IncomingMessage, | ||
ResponseInternal extends ServerResponse = ServerResponse, | ||
>( | ||
@@ -171,2 +174,4 @@ compiler: Compiler | MultiCompiler, | ||
export { | ||
hapiWrapper, | ||
koaWrapper, | ||
Schema, | ||
@@ -206,2 +211,5 @@ Compiler, | ||
WithoutUndefined, | ||
HapiPluginBase, | ||
HapiPlugin, | ||
HapiOptions, | ||
}; | ||
@@ -212,6 +220,45 @@ } | ||
type API< | ||
RequestInternal extends import("http").IncomingMessage, | ||
ResponseInternal extends ServerResponse, | ||
RequestInternal extends | ||
import("http").IncomingMessage = import("http").IncomingMessage, | ||
ResponseInternal extends ServerResponse = ServerResponse, | ||
> = Middleware<RequestInternal, ResponseInternal> & | ||
AdditionalMethods<RequestInternal, ResponseInternal>; | ||
/** | ||
* @template S | ||
* @template O | ||
* @typedef {Object} HapiPluginBase | ||
* @property {(server: S, options: O) => void | Promise<void>} register | ||
*/ | ||
/** | ||
* @template S | ||
* @template O | ||
* @typedef {HapiPluginBase<S, O> & { pkg: { name: string } }} HapiPlugin | ||
*/ | ||
/** | ||
* @typedef {Options & { compiler: Compiler | MultiCompiler }} HapiOptions | ||
*/ | ||
/** | ||
* @template HapiServer | ||
* @template {HapiOptions} HapiOptionsInternal | ||
* @returns {HapiPlugin<HapiServer, HapiOptionsInternal>} | ||
*/ | ||
declare function hapiWrapper< | ||
HapiServer, | ||
HapiOptionsInternal extends HapiOptions, | ||
>(): HapiPlugin<HapiServer, HapiOptionsInternal>; | ||
/** | ||
* @template {IncomingMessage} [RequestInternal=IncomingMessage] | ||
* @template {ServerResponse} [ResponseInternal=ServerResponse] | ||
* @param {Compiler | MultiCompiler} compiler | ||
* @param {Options<RequestInternal, ResponseInternal>} [options] | ||
* @returns {(ctx: any, next: Function) => Promise<void> | void} | ||
*/ | ||
declare function koaWrapper< | ||
RequestInternal extends | ||
import("http").IncomingMessage = import("http").IncomingMessage, | ||
ResponseInternal extends ServerResponse = ServerResponse, | ||
>( | ||
compiler: Compiler | MultiCompiler, | ||
options?: Options<RequestInternal, ResponseInternal> | undefined, | ||
): (ctx: any, next: Function) => Promise<void> | void; | ||
type Schema = import("schema-utils/declarations/validate").Schema; | ||
@@ -252,17 +299,19 @@ type Configuration = import("webpack").Configuration; | ||
type ResponseData = { | ||
data: string | Buffer | ReadStream; | ||
data: Buffer | ReadStream; | ||
byteLength: number; | ||
}; | ||
type ModifyResponseData< | ||
RequestInternal extends import("http").IncomingMessage, | ||
ResponseInternal extends ServerResponse, | ||
RequestInternal extends | ||
import("http").IncomingMessage = import("http").IncomingMessage, | ||
ResponseInternal extends ServerResponse = ServerResponse, | ||
> = ( | ||
req: RequestInternal, | ||
res: ResponseInternal, | ||
data: string | Buffer | ReadStream, | ||
data: Buffer | ReadStream, | ||
byteLength: number, | ||
) => ResponseData; | ||
type Context< | ||
RequestInternal extends import("http").IncomingMessage, | ||
ResponseInternal extends ServerResponse, | ||
RequestInternal extends | ||
import("http").IncomingMessage = import("http").IncomingMessage, | ||
ResponseInternal extends ServerResponse = ServerResponse, | ||
> = { | ||
@@ -279,4 +328,5 @@ state: boolean; | ||
type FilledContext< | ||
RequestInternal extends import("http").IncomingMessage, | ||
ResponseInternal extends ServerResponse, | ||
RequestInternal extends | ||
import("http").IncomingMessage = import("http").IncomingMessage, | ||
ResponseInternal extends ServerResponse = ServerResponse, | ||
> = WithoutUndefined<Context<RequestInternal, ResponseInternal>, "watching">; | ||
@@ -290,4 +340,5 @@ type NormalizedHeaders = | ||
type Headers< | ||
RequestInternal extends import("http").IncomingMessage, | ||
ResponseInternal extends ServerResponse, | ||
RequestInternal extends | ||
import("http").IncomingMessage = import("http").IncomingMessage, | ||
ResponseInternal extends ServerResponse = ServerResponse, | ||
> = | ||
@@ -302,4 +353,5 @@ | NormalizedHeaders | ||
type Options< | ||
RequestInternal extends import("http").IncomingMessage, | ||
ResponseInternal extends ServerResponse, | ||
RequestInternal extends | ||
import("http").IncomingMessage = import("http").IncomingMessage, | ||
ResponseInternal extends ServerResponse = ServerResponse, | ||
> = { | ||
@@ -323,6 +375,9 @@ mimeTypes?: | ||
| undefined; | ||
etag?: "strong" | "weak" | undefined; | ||
lastModified?: boolean | undefined; | ||
}; | ||
type Middleware< | ||
RequestInternal extends import("http").IncomingMessage, | ||
ResponseInternal extends ServerResponse, | ||
RequestInternal extends | ||
import("http").IncomingMessage = import("http").IncomingMessage, | ||
ResponseInternal extends ServerResponse = ServerResponse, | ||
> = ( | ||
@@ -355,1 +410,12 @@ req: RequestInternal, | ||
}; | ||
type HapiPluginBase<S, O> = { | ||
register: (server: S, options: O) => void | Promise<void>; | ||
}; | ||
type HapiPlugin<S, O> = HapiPluginBase<S, O> & { | ||
pkg: { | ||
name: string; | ||
}; | ||
}; | ||
type HapiOptions = Options & { | ||
compiler: Compiler | MultiCompiler; | ||
}; |
@@ -6,2 +6,9 @@ /// <reference types="node" /> | ||
* @template {ServerResponse} Response | ||
* @typedef {Object} SendErrorOptions send error options | ||
* @property {Record<string, number | string | string[] | undefined>=} headers headers | ||
* @property {import("./index").ModifyResponseData<Request, Response>=} modifyResponseData modify response data callback | ||
*/ | ||
/** | ||
* @template {IncomingMessage} Request | ||
* @template {ServerResponse} Response | ||
* @param {import("./index.js").FilledContext<Request, Response>} context | ||
@@ -17,4 +24,29 @@ * @return {import("./index.js").Middleware<Request, Response>} | ||
declare namespace wrapper { | ||
export { NextFunction, IncomingMessage, ServerResponse, NormalizedHeaders }; | ||
export { | ||
SendErrorOptions, | ||
NextFunction, | ||
IncomingMessage, | ||
ServerResponse, | ||
NormalizedHeaders, | ||
ReadStream, | ||
}; | ||
} | ||
/** | ||
* send error options | ||
*/ | ||
type SendErrorOptions< | ||
Request extends import("http").IncomingMessage, | ||
Response extends import("./index.js").ServerResponse, | ||
> = { | ||
/** | ||
* headers | ||
*/ | ||
headers?: Record<string, number | string | string[] | undefined> | undefined; | ||
/** | ||
* modify response data callback | ||
*/ | ||
modifyResponseData?: | ||
| import("./index").ModifyResponseData<Request, Response> | ||
| undefined; | ||
}; | ||
type NextFunction = import("./index.js").NextFunction; | ||
@@ -24,1 +56,2 @@ type IncomingMessage = import("./index.js").IncomingMessage; | ||
type NormalizedHeaders = import("./index.js").NormalizedHeaders; | ||
type ReadStream = import("fs").ReadStream; |
/// <reference types="node" /> | ||
export type IncomingMessage = import("../index.js").IncomingMessage; | ||
export type ServerResponse = import("../index.js").ServerResponse; | ||
export type ReadStream = import("fs").ReadStream; | ||
export type ExpectedRequest = { | ||
get: (name: string) => string | undefined; | ||
}; | ||
export type ExpectedResponse = { | ||
get: (name: string) => string | string[] | undefined; | ||
set: (name: string, value: number | string | string[]) => void; | ||
status: (status: number) => void; | ||
send: (data: any) => void; | ||
status?: ((status: number) => void) | undefined; | ||
send?: ((data: any) => void) | undefined; | ||
pipeInto?: ((data: any) => void) | undefined; | ||
}; | ||
/** | ||
* send error options | ||
*/ | ||
export type SendOptions< | ||
Request extends import("http").IncomingMessage, | ||
Response extends import("../index.js").ServerResponse, | ||
> = { | ||
/** | ||
* headers | ||
*/ | ||
headers?: Record<string, number | string | string[] | undefined> | undefined; | ||
/** | ||
* modify response data callback | ||
*/ | ||
modifyResponseData?: | ||
| import("../index").ModifyResponseData<Request, Response> | ||
| undefined; | ||
/** | ||
* modify response data callback | ||
*/ | ||
outputFileSystem: import("../index").OutputFileSystem; | ||
}; | ||
/** @typedef {import("../index.js").IncomingMessage} IncomingMessage */ | ||
/** @typedef {import("../index.js").ServerResponse} ServerResponse */ | ||
/** @typedef {import("fs").ReadStream} ReadStream */ | ||
/** | ||
* @typedef {Object} ExpectedRequest | ||
* @property {(name: string) => string | undefined} get | ||
*/ | ||
/** | ||
* @typedef {Object} ExpectedResponse | ||
* @property {(name: string) => string | string[] | undefined} get | ||
* @property {(name: string, value: number | string | string[]) => void} set | ||
* @property {(status: number) => void} status | ||
* @property {(data: any) => void} send | ||
* @property {(status: number) => void} [status] | ||
* @property {(data: any) => void} [send] | ||
* @property {(data: any) => void} [pipeInto] | ||
*/ | ||
/** | ||
* @template {ServerResponse} Response | ||
* @template {ServerResponse & ExpectedResponse} Response | ||
* @param {Response} res | ||
* @returns {string[]} | ||
* @param {number} code | ||
*/ | ||
export function getHeaderNames< | ||
Response extends import("../index.js").ServerResponse, | ||
>(res: Response): string[]; | ||
export function setStatusCode< | ||
Response extends import("http").ServerResponse< | ||
import("http").IncomingMessage | ||
> & | ||
import("../index.js").ExtendedServerResponse & | ||
ExpectedResponse, | ||
>(res: Response, code: number): void; | ||
/** | ||
* @template {IncomingMessage} Request | ||
* @param {Request} req | ||
* @param {string} name | ||
* @returns {string | string[] | undefined} | ||
* @template {ServerResponse} Response | ||
* @param {Response & ExpectedResponse} res | ||
* @param {string | Buffer} bufferOrStream | ||
*/ | ||
export function getHeaderFromRequest< | ||
export function send< | ||
Request extends import("http").IncomingMessage, | ||
>(req: Request, name: string): string | string[] | undefined; | ||
/** | ||
* @template {ServerResponse} Response | ||
* @param {Response} res | ||
* @param {string} name | ||
* @returns {number | string | string[] | undefined} | ||
*/ | ||
export function getHeaderFromResponse< | ||
Response extends import("../index.js").ServerResponse, | ||
>(res: Response, name: string): number | string | string[] | undefined; | ||
>(res: Response & ExpectedResponse, bufferOrStream: string | Buffer): void; | ||
/** | ||
* @template {ServerResponse} Response | ||
* @param {Response} res | ||
* @param {string} name | ||
* @param {number | string | string[]} value | ||
* @returns {void} | ||
* @param {Response & ExpectedResponse} res | ||
* @param {import("fs").ReadStream} bufferOrStream | ||
*/ | ||
export function setHeaderForResponse< | ||
Response extends import("../index.js").ServerResponse, | ||
>(res: Response, name: string, value: number | string | string[]): void; | ||
export function pipe<Response extends import("../index.js").ServerResponse>( | ||
res: Response & ExpectedResponse, | ||
bufferOrStream: import("fs").ReadStream, | ||
): void; | ||
/** | ||
* @template {ServerResponse} Response | ||
* @param {Response} res | ||
* @param {number} code | ||
*/ | ||
export function setStatusCode< | ||
Response extends import("../index.js").ServerResponse, | ||
>(res: Response, code: number): void; | ||
/** | ||
* @template {IncomingMessage} Request | ||
* @template {ServerResponse} Response | ||
* @typedef {Object} SendOptions send error options | ||
* @property {Record<string, number | string | string[] | undefined>=} headers headers | ||
* @property {import("../index").ModifyResponseData<Request, Response>=} modifyResponseData modify response data callback | ||
* @property {import("../index").OutputFileSystem} outputFileSystem modify response data callback | ||
*/ | ||
/** | ||
* @template {IncomingMessage} Request | ||
* @template {ServerResponse} Response | ||
* @param {Request} req | ||
* @param {Response} res | ||
* @param {string} filename | ||
* @param {import("../index").OutputFileSystem} outputFileSystem | ||
* @param {number} start | ||
* @param {number} end | ||
* @param {() => Promise<void>} goNext | ||
* @param {SendOptions<Request, Response>} options | ||
* @returns {{ bufferOrStream: (Buffer | import("fs").ReadStream), byteLength: number }} | ||
*/ | ||
export function send< | ||
Request extends import("http").IncomingMessage, | ||
Response extends import("../index.js").ServerResponse, | ||
>( | ||
req: Request, | ||
res: Response, | ||
export function createReadStreamOrReadFileSync( | ||
filename: string, | ||
outputFileSystem: import("../index").OutputFileSystem, | ||
start: number, | ||
end: number, | ||
goNext: () => Promise<void>, | ||
options: SendOptions<Request, Response>, | ||
): Promise<void>; | ||
/** | ||
* @template {IncomingMessage} Request | ||
* @template {ServerResponse} Response | ||
* @param {Request} req response | ||
* @param {Response} res response | ||
* @param {number} status status | ||
* @param {Partial<SendOptions<Request, Response>>=} options options | ||
* @returns {void} | ||
*/ | ||
export function sendError< | ||
Request extends import("http").IncomingMessage, | ||
Response extends import("../index.js").ServerResponse, | ||
>( | ||
req: Request, | ||
res: Response, | ||
status: number, | ||
options?: Partial<SendOptions<Request, Response>> | undefined, | ||
): void; | ||
): { | ||
bufferOrStream: Buffer | import("fs").ReadStream; | ||
byteLength: number; | ||
}; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
112703
30
2561
700
40