first commit

This commit is contained in:
monjack
2025-06-20 18:01:48 +08:00
commit 6daa6d65c1
24611 changed files with 2512443 additions and 0 deletions

View File

@ -0,0 +1,305 @@
"use strict";
const {
validate
} = require("schema-utils");
const mime = require("mime-types");
const middleware = require("./middleware");
const getFilenameFromUrl = require("./utils/getFilenameFromUrl");
const setupHooks = require("./utils/setupHooks");
const setupWriteToDisk = require("./utils/setupWriteToDisk");
const setupOutputFileSystem = require("./utils/setupOutputFileSystem");
const ready = require("./utils/ready");
const schema = require("./options.json");
const noop = () => {};
/** @typedef {import("schema-utils/declarations/validate").Schema} Schema */
/** @typedef {import("webpack").Compiler} Compiler */
/** @typedef {import("webpack").MultiCompiler} MultiCompiler */
/** @typedef {import("webpack").Configuration} Configuration */
/** @typedef {import("webpack").Stats} Stats */
/** @typedef {import("webpack").MultiStats} MultiStats */
/**
* @typedef {Object} ExtendedServerResponse
* @property {{ webpack?: { devMiddleware?: Context<IncomingMessage, ServerResponse> } }} [locals]
*/
/** @typedef {import("http").IncomingMessage} IncomingMessage */
/** @typedef {import("http").ServerResponse & ExtendedServerResponse} ServerResponse */
/**
* @callback NextFunction
* @param {any} [err]
* @return {void}
*/
/**
* @typedef {NonNullable<Configuration["watchOptions"]>} WatchOptions
*/
/**
* @typedef {Compiler["watching"]} Watching
*/
/**
* @typedef {ReturnType<Compiler["watch"]>} MultiWatching
*/
/**
* @typedef {Compiler["outputFileSystem"] & { createReadStream?: import("fs").createReadStream, statSync?: import("fs").statSync, lstat?: import("fs").lstat, readFileSync?: import("fs").readFileSync }} OutputFileSystem
*/
/** @typedef {ReturnType<Compiler["getInfrastructureLogger"]>} Logger */
/**
* @callback Callback
* @param {Stats | MultiStats} [stats]
*/
/**
* @template {IncomingMessage} Request
* @template {ServerResponse} Response
* @typedef {Object} Context
* @property {boolean} state
* @property {Stats | MultiStats | undefined} stats
* @property {Callback[]} callbacks
* @property {Options<Request, Response>} options
* @property {Compiler | MultiCompiler} compiler
* @property {Watching | MultiWatching} watching
* @property {Logger} logger
* @property {OutputFileSystem} outputFileSystem
*/
/**
* @template {IncomingMessage} Request
* @template {ServerResponse} Response
* @typedef {Record<string, string | number> | Array<{ key: string, value: number | string }> | ((req: Request, res: Response, context: Context<Request, Response>) => void | undefined | Record<string, string | number>) | undefined} Headers
*/
/**
* @template {IncomingMessage} Request
* @template {ServerResponse} Response
* @typedef {Object} Options
* @property {{[key: string]: string}} [mimeTypes]
* @property {boolean | ((targetPath: string) => boolean)} [writeToDisk]
* @property {string} [methods]
* @property {Headers<Request, Response>} [headers]
* @property {NonNullable<Configuration["output"]>["publicPath"]} [publicPath]
* @property {Configuration["stats"]} [stats]
* @property {boolean} [serverSideRender]
* @property {OutputFileSystem} [outputFileSystem]
* @property {boolean | string} [index]
*/
/**
* @template {IncomingMessage} Request
* @template {ServerResponse} Response
* @callback Middleware
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @return {Promise<void>}
*/
/**
* @callback GetFilenameFromUrl
* @param {string} url
* @returns {string | undefined}
*/
/**
* @callback WaitUntilValid
* @param {Callback} callback
*/
/**
* @callback Invalidate
* @param {Callback} callback
*/
/**
* @callback Close
* @param {(err: Error | null | undefined) => void} callback
*/
/**
* @template {IncomingMessage} Request
* @template {ServerResponse} Response
* @typedef {Object} AdditionalMethods
* @property {GetFilenameFromUrl} getFilenameFromUrl
* @property {WaitUntilValid} waitUntilValid
* @property {Invalidate} invalidate
* @property {Close} close
* @property {Context<Request, Response>} context
*/
/**
* @template {IncomingMessage} Request
* @template {ServerResponse} Response
* @typedef {Middleware<Request, Response> & AdditionalMethods<Request, Response>} API
*/
/**
* @template {IncomingMessage} Request
* @template {ServerResponse} Response
* @param {Compiler | MultiCompiler} compiler
* @param {Options<Request, Response>} [options]
* @returns {API<Request, Response>}
*/
function wdm(compiler, options = {}) {
validate(
/** @type {Schema} */
schema, options, {
name: "Dev Middleware",
baseDataPath: "options"
});
const {
mimeTypes
} = options;
if (mimeTypes) {
const {
types
} = mime; // mimeTypes from user provided options should take priority
// over existing, known types
// @ts-ignore
mime.types = { ...types,
...mimeTypes
};
}
/**
* @type {Context<Request, Response>}
*/
const context = {
state: false,
// eslint-disable-next-line no-undefined
stats: undefined,
callbacks: [],
options,
compiler,
// @ts-ignore
// eslint-disable-next-line no-undefined
watching: undefined,
logger: compiler.getInfrastructureLogger("webpack-dev-middleware"),
// @ts-ignore
// eslint-disable-next-line no-undefined
outputFileSystem: undefined
};
setupHooks(context);
if (options.writeToDisk) {
setupWriteToDisk(context);
}
setupOutputFileSystem(context); // Start watching
if (
/** @type {Compiler} */
context.compiler.watching) {
context.watching =
/** @type {Compiler} */
context.compiler.watching;
} else {
/**
* @type {WatchOptions | WatchOptions[]}
*/
let watchOptions;
/**
* @param {Error | null | undefined} error
*/
const errorHandler = error => {
if (error) {
// TODO: improve that in future
// For example - `writeToDisk` can throw an error and right now it is ends watching.
// We can improve that and keep watching active, but it is require API on webpack side.
// Let's implement that in webpack@5 because it is rare case.
context.logger.error(error);
}
};
if (Array.isArray(
/** @type {MultiCompiler} */
context.compiler.compilers)) {
watchOptions =
/** @type {MultiCompiler} */
context.compiler.compilers.map(
/**
* @param {Compiler} childCompiler
* @returns {WatchOptions}
*/
childCompiler => childCompiler.options.watchOptions || {});
context.watching =
/** @type {MultiWatching} */
context.compiler.watch(
/** @type {WatchOptions}} */
watchOptions, errorHandler);
} else {
watchOptions =
/** @type {Compiler} */
context.compiler.options.watchOptions || {};
context.watching =
/** @type {Watching} */
context.compiler.watch(watchOptions, errorHandler);
}
}
const instance =
/** @type {API<Request, Response>} */
middleware(context); // API
/** @type {API<Request, Response>} */
instance.getFilenameFromUrl =
/**
* @param {string} url
* @returns {string|undefined}
*/
url => getFilenameFromUrl(context, url);
/** @type {API<Request, Response>} */
instance.waitUntilValid = (callback = noop) => {
ready(context, callback);
};
/** @type {API<Request, Response>} */
instance.invalidate = (callback = noop) => {
ready(context, callback);
context.watching.invalidate();
};
/** @type {API<Request, Response>} */
instance.close = (callback = noop) => {
context.watching.close(callback);
};
/** @type {API<Request, Response>} */
instance.context = context;
return instance;
}
module.exports = wdm;

View File

@ -0,0 +1,249 @@
"use strict";
const path = require("path");
const mime = require("mime-types");
const parseRange = require("range-parser");
const getFilenameFromUrl = require("./utils/getFilenameFromUrl");
const {
getHeaderNames,
getHeaderFromRequest,
getHeaderFromResponse,
setHeaderForResponse,
setStatusCode,
send,
sendError
} = require("./utils/compatibleAPI");
const ready = require("./utils/ready");
/** @typedef {import("./index.js").NextFunction} NextFunction */
/** @typedef {import("./index.js").IncomingMessage} IncomingMessage */
/** @typedef {import("./index.js").ServerResponse} ServerResponse */
/**
* @param {string} type
* @param {number} size
* @param {import("range-parser").Range} [range]
* @returns {string}
*/
function getValueContentRangeHeader(type, size, range) {
return `${type} ${range ? `${range.start}-${range.end}` : "*"}/${size}`;
}
/**
* @param {string | number} title
* @param {string} body
* @returns {string}
*/
function createHtmlDocument(title, body) {
return `${"<!DOCTYPE html>\n" + '<html lang="en">\n' + "<head>\n" + '<meta charset="utf-8">\n' + "<title>"}${title}</title>\n` + `</head>\n` + `<body>\n` + `<pre>${body}</pre>\n` + `</body>\n` + `</html>\n`;
}
const BYTES_RANGE_REGEXP = /^ *bytes/i;
/**
* @template {IncomingMessage} Request
* @template {ServerResponse} Response
* @param {import("./index.js").Context<Request, Response>} context
* @return {import("./index.js").Middleware<Request, Response>}
*/
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 || {};
if (req.method && !acceptedMethods.includes(req.method)) {
await goNext();
return;
}
ready(context, processRequest, req);
async function goNext() {
if (!context.options.serverSideRender) {
return next();
}
return new Promise(resolve => {
ready(context, () => {
/** @type {any} */
// eslint-disable-next-line no-param-reassign
res.locals.webpack = {
devMiddleware: context
};
resolve(next());
}, req);
});
}
async function processRequest() {
/** @type {import("./utils/getFilenameFromUrl").Extra} */
const extra = {};
const filename = getFilenameFromUrl(context,
/** @type {string} */
req.url, extra);
if (!filename) {
await goNext();
return;
}
if (extra.errorCode) {
if (extra.errorCode === 403) {
context.logger.error(`Malicious path "${filename}".`);
}
sendError(req, res, extra.errorCode);
return;
}
let {
headers
} = context.options;
if (typeof headers === "function") {
// @ts-ignore
headers = headers(req, res, context);
}
/**
* @type {{key: string, value: string | number}[]}
*/
const allHeaders = [];
if (typeof headers !== "undefined") {
if (!Array.isArray(headers)) {
// eslint-disable-next-line guard-for-in
for (const name in headers) {
// @ts-ignore
allHeaders.push({
key: name,
value: headers[name]
});
}
headers = allHeaders;
}
headers.forEach(
/**
* @param {{key: string, value: any}} header
*/
header => {
setHeaderForResponse(res, header.key, header.value);
});
}
if (!getHeaderFromResponse(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) {
setHeaderForResponse(res, "Content-Type", contentType);
}
}
if (!getHeaderFromResponse(res, "Accept-Ranges")) {
setHeaderForResponse(res, "Accept-Ranges", "bytes");
}
const rangeHeader = getHeaderFromRequest(req, "range");
let start;
let end;
if (rangeHeader && BYTES_RANGE_REGEXP.test(rangeHeader)) {
const size = await new Promise(resolve => {
/** @type {import("fs").lstat} */
context.outputFileSystem.lstat(filename, (error, stats) => {
if (error) {
context.logger.error(error);
return;
}
resolve(stats.size);
});
});
const parsedRanges = parseRange(size, rangeHeader, {
combine: true
});
if (parsedRanges === -1) {
const message = "Unsatisfiable range for 'Range' header.";
context.logger.error(message);
const existingHeaders = getHeaderNames(res);
for (let i = 0; i < existingHeaders.length; i++) {
res.removeHeader(existingHeaders[i]);
}
setStatusCode(res, 416);
setHeaderForResponse(res, "Content-Range", getValueContentRangeHeader("bytes", size));
setHeaderForResponse(res, "Content-Type", "text/html; charset=utf-8");
const document = createHtmlDocument(416, `Error: ${message}`);
const byteLength = Buffer.byteLength(document);
setHeaderForResponse(res, "Content-Length", Buffer.byteLength(document));
send(req, res, document, byteLength);
return;
} else if (parsedRanges === -2) {
context.logger.error("A malformed 'Range' header was provided. A regular response will be sent for this request.");
} else if (parsedRanges.length > 1) {
context.logger.error("A 'Range' header with multiple ranges was provided. Multiple ranges are not supported, so a regular response will be sent for this request.");
}
if (parsedRanges !== -2 && parsedRanges.length === 1) {
// Content-Range
setStatusCode(res, 206);
setHeaderForResponse(res, "Content-Range", getValueContentRangeHeader("bytes", size,
/** @type {import("range-parser").Ranges} */
parsedRanges[0]));
[{
start,
end
}] = parsedRanges;
}
}
const isFsSupportsStream = typeof context.outputFileSystem.createReadStream === "function";
let bufferOtStream;
let byteLength;
try {
if (typeof start !== "undefined" && typeof end !== "undefined" && isFsSupportsStream) {
bufferOtStream =
/** @type {import("fs").createReadStream} */
context.outputFileSystem.createReadStream(filename, {
start,
end
});
byteLength = end - start + 1;
} else {
bufferOtStream =
/** @type {import("fs").readFileSync} */
context.outputFileSystem.readFileSync(filename);
({
byteLength
} = bufferOtStream);
}
} catch (_ignoreError) {
await goNext();
return;
}
send(req, res, bufferOtStream, byteLength);
}
};
}
module.exports = wrapper;

View File

@ -0,0 +1,125 @@
{
"type": "object",
"properties": {
"mimeTypes": {
"description": "Allows a user to register custom mime types or extension mappings.",
"link": "https://github.com/webpack/webpack-dev-middleware#mimetypes",
"type": "object"
},
"writeToDisk": {
"description": "Allows to write generated files on disk.",
"link": "https://github.com/webpack/webpack-dev-middleware#writetodisk",
"anyOf": [
{
"type": "boolean"
},
{
"instanceof": "Function"
}
]
},
"methods": {
"description": "Allows to pass the list of HTTP request methods accepted by the middleware.",
"link": "https://github.com/webpack/webpack-dev-middleware#methods",
"type": "array",
"items": {
"type": "string",
"minlength": "1"
}
},
"headers": {
"anyOf": [
{
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"key": {
"description": "key of header.",
"type": "string"
},
"value": {
"description": "value of header.",
"type": "string"
}
}
},
"minItems": 1
},
{
"type": "object"
},
{
"instanceof": "Function"
}
],
"description": "Allows to pass custom HTTP headers on each request",
"link": "https://github.com/webpack/webpack-dev-middleware#headers"
},
"publicPath": {
"description": "The `publicPath` specifies the public URL address of the output files when referenced in a browser.",
"link": "https://github.com/webpack/webpack-dev-middleware#publicpath",
"anyOf": [
{
"enum": ["auto"]
},
{
"type": "string"
},
{
"instanceof": "Function"
}
]
},
"stats": {
"description": "Stats options object or preset name.",
"link": "https://github.com/webpack/webpack-dev-middleware#stats",
"anyOf": [
{
"enum": [
"none",
"summary",
"errors-only",
"errors-warnings",
"minimal",
"normal",
"detailed",
"verbose"
]
},
{
"type": "boolean"
},
{
"type": "object",
"additionalProperties": true
}
]
},
"serverSideRender": {
"description": "Instructs the module to enable or disable the server-side rendering mode.",
"link": "https://github.com/webpack/webpack-dev-middleware#serversiderender",
"type": "boolean"
},
"outputFileSystem": {
"description": "Set the default file system which will be used by webpack as primary destination of generated files.",
"link": "https://github.com/webpack/webpack-dev-middleware#outputfilesystem",
"type": "object"
},
"index": {
"description": "Allows to serve an index of the directory.",
"link": "https://github.com/webpack/webpack-dev-middleware#index",
"anyOf": [
{
"type": "boolean"
},
{
"type": "string",
"minlength": "1"
}
]
}
},
"additionalProperties": false
}

View File

@ -0,0 +1,292 @@
"use strict";
/** @typedef {import("../index.js").IncomingMessage} IncomingMessage */
/** @typedef {import("../index.js").ServerResponse} ServerResponse */
/**
* @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
*/
/**
* @template {ServerResponse} 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 | 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
// @ts-ignore
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 {number} code
*/
function setStatusCode(res, code) {
if (typeof
/** @type {Response & ExpectedResponse} */
res.status === "function") {
/** @type {Response & ExpectedResponse} */
res.status(code);
return;
} // eslint-disable-next-line no-param-reassign
res.statusCode = code;
}
/**
* @template {IncomingMessage} Request
* @template {ServerResponse} Response
* @param {Request} req
* @param {Response} res
* @param {string | Buffer | import("fs").ReadStream} bufferOtStream
* @param {number} byteLength
*/
function send(req, res, bufferOtStream, byteLength) {
if (typeof
/** @type {import("fs").ReadStream} */
bufferOtStream.pipe === "function") {
setHeaderForResponse(res, "Content-Length", byteLength);
if (req.method === "HEAD") {
res.end();
return;
}
/** @type {import("fs").ReadStream} */
bufferOtStream.pipe(res);
return;
}
if (typeof
/** @type {Response & ExpectedResponse} */
res.send === "function") {
/** @type {Response & ExpectedResponse} */
res.send(bufferOtStream);
return;
} // Only Node.js API used
res.setHeader("Content-Length", byteLength);
if (req.method === "HEAD") {
res.end();
} else {
res.end(bufferOtStream);
}
}
/**
* @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]);
}
}
const matchHtmlRegExp = /["'&<>]/;
/**
* @param {string} string raw HTML
* @returns {string} escaped HTML
*/
function escapeHtml(string) {
const str = `${string}`;
const match = matchHtmlRegExp.exec(str);
if (!match) {
return str;
}
let escape;
let html = "";
let index = 0;
let lastIndex = 0;
for (({
index
} = match); index < str.length; index++) {
switch (str.charCodeAt(index)) {
// "
case 34:
escape = "&quot;";
break;
// &
case 38:
escape = "&amp;";
break;
// '
case 39:
escape = "&#39;";
break;
// <
case 60:
escape = "&lt;";
break;
// >
case 62:
escape = "&gt;";
break;
default:
// eslint-disable-next-line no-continue
continue;
}
if (lastIndex !== index) {
html += str.substring(lastIndex, index);
}
lastIndex = index + 1;
html += escape;
}
return lastIndex !== index ? html + str.substring(lastIndex, index) : html;
}
/** @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
* @returns {void}
*/
function sendError(req, res, status) {
const content = statuses[status] || String(status);
const 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); // 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");
const byteLength = Buffer.byteLength(document);
setHeaderForResponse(res, "Content-Length", byteLength);
res.end(document);
}
module.exports = {
getHeaderNames,
getHeaderFromRequest,
getHeaderFromResponse,
setHeaderForResponse,
setStatusCode,
send,
sendError
};

View File

@ -0,0 +1,195 @@
"use strict";
const path = require("path");
const {
parse
} = require("url");
const querystring = require("querystring");
const getPaths = require("./getPaths");
/** @typedef {import("../index.js").IncomingMessage} IncomingMessage */
/** @typedef {import("../index.js").ServerResponse} ServerResponse */
const cacheStore = new WeakMap();
/**
* @template T
* @param {Function} fn
* @param {{ cache?: Map<string, { data: T }> } | undefined} cache
* @param {(value: T) => T} callback
* @returns {any}
*/
// @ts-ignore
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 => {
if (value.pathname) {
// eslint-disable-next-line no-param-reassign
value.pathname = decode(value.pathname);
}
return value;
});
const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
/**
* @typedef {Object} Extra
* @property {import("fs").Stats=} stats
* @property {number=} errorCode
*/
/**
* decodeURIComponent.
*
* Allows V8 to only deoptimize this fn instead of all of send().
*
* @param {string} input
* @returns {string}
*/
function decode(input) {
return querystring.unescape(input);
}
/**
* @template {IncomingMessage} Request
* @template {ServerResponse} Response
* @param {import("../index.js").Context<Request, Response>} context
* @param {string} url
* @param {Extra=} extra
* @returns {string | undefined}
*/
function getFilenameFromUrl(context, url, extra = {}) {
const {
options
} = context;
const paths = getPaths(context);
/** @type {string | undefined} */
let foundFilename;
/** @type {URL} */
let urlObject;
try {
// The `url` property of the `request` is contains only `pathname`, `search` and `hash`
urlObject = memoizedParse(url, false, true);
} catch (_ignoreError) {
return;
}
for (const {
publicPath,
outputPath
} of paths) {
/** @type {string | undefined} */
let filename;
/** @type {URL} */
let publicPathObject;
try {
publicPathObject = memoizedParse(publicPath !== "auto" && publicPath ? publicPath : "/", false, true);
} catch (_ignoreError) {
// eslint-disable-next-line no-continue
continue;
}
const {
pathname
} = urlObject;
const {
pathname: publicPathPathname
} = publicPathObject;
if (pathname && pathname.startsWith(publicPathPathname)) {
// Null byte(s)
if (pathname.includes("\0")) {
// eslint-disable-next-line no-param-reassign
extra.errorCode = 400;
return;
} // ".." is malicious
if (UP_PATH_REGEXP.test(path.normalize(`./${pathname}`))) {
// eslint-disable-next-line no-param-reassign
extra.errorCode = 403;
return;
} // Strip the `pathname` property from the `publicPath` option from the start of requested url
// `/complex/foo.js` => `foo.js`
// and add outputPath
// `foo.js` => `/home/user/my-project/dist/foo.js`
filename = path.join(outputPath, pathname.slice(publicPathPathname.length));
try {
// eslint-disable-next-line no-param-reassign
extra.stats =
/** @type {import("fs").statSync} */
context.outputFileSystem.statSync(filename);
} catch (_ignoreError) {
// eslint-disable-next-line no-continue
continue;
}
if (extra.stats.isFile()) {
foundFilename = filename;
break;
} else if (extra.stats.isDirectory() && (typeof options.index === "undefined" || options.index)) {
const indexValue = typeof options.index === "undefined" || typeof options.index === "boolean" ? "index.html" : options.index;
filename = path.join(filename, indexValue);
try {
// eslint-disable-next-line no-param-reassign
extra.stats =
/** @type {import("fs").statSync} */
context.outputFileSystem.statSync(filename);
} catch (__ignoreError) {
// eslint-disable-next-line no-continue
continue;
}
if (extra.stats.isFile()) {
foundFilename = filename;
break;
}
}
}
} // eslint-disable-next-line consistent-return
return foundFilename;
}
module.exports = getFilenameFromUrl;

View File

@ -0,0 +1,49 @@
"use strict";
/** @typedef {import("webpack").Compiler} Compiler */
/** @typedef {import("webpack").Stats} Stats */
/** @typedef {import("webpack").MultiStats} MultiStats */
/** @typedef {import("../index.js").IncomingMessage} IncomingMessage */
/** @typedef {import("../index.js").ServerResponse} ServerResponse */
/**
* @template {IncomingMessage} Request
* @template {ServerResponse} Response
* @param {import("../index.js").Context<Request, Response>} context
*/
function getPaths(context) {
const {
stats,
options
} = context;
/** @type {Stats[]} */
const childStats =
/** @type {MultiStats} */
stats.stats ?
/** @type {MultiStats} */
stats.stats : [
/** @type {Stats} */
stats];
const publicPaths = [];
for (const {
compilation
} of childStats) {
// The `output.path` is always present and always absolute
const outputPath = compilation.getPath(compilation.outputOptions.path || "");
const publicPath = options.publicPath ? compilation.getPath(options.publicPath) : compilation.outputOptions.publicPath ? compilation.getPath(compilation.outputOptions.publicPath) : "";
publicPaths.push({
outputPath,
publicPath
});
}
return publicPaths;
}
module.exports = getPaths;

View File

@ -0,0 +1,26 @@
"use strict";
/** @typedef {import("../index.js").IncomingMessage} IncomingMessage */
/** @typedef {import("../index.js").ServerResponse} ServerResponse */
/**
* @template {IncomingMessage} Request
* @template {ServerResponse} Response
* @param {import("../index.js").Context<Request, Response>} context
* @param {(...args: any[]) => any} callback
* @param {Request} [req]
* @returns {void}
*/
function ready(context, callback, req) {
if (context.state) {
callback(context.stats);
return;
}
const name = req && req.url || callback.name;
context.logger.info(`wait until bundle finished${name ? `: ${name}` : ""}`);
context.callbacks.push(callback);
}
module.exports = ready;

View File

@ -0,0 +1,216 @@
"use strict";
const webpack = require("webpack");
const {
isColorSupported
} = require("colorette");
/** @typedef {import("webpack").Configuration} Configuration */
/** @typedef {import("webpack").Compiler} Compiler */
/** @typedef {import("webpack").MultiCompiler} MultiCompiler */
/** @typedef {import("webpack").Stats} Stats */
/** @typedef {import("webpack").MultiStats} MultiStats */
/** @typedef {import("../index.js").IncomingMessage} IncomingMessage */
/** @typedef {import("../index.js").ServerResponse} ServerResponse */
/** @typedef {Configuration["stats"]} StatsOptions */
/** @typedef {{ children: Configuration["stats"][] }} MultiStatsOptions */
/** @typedef {Exclude<Configuration["stats"], boolean | string | undefined>} NormalizedStatsOptions */
// TODO remove `color` after dropping webpack v4
/** @typedef {{ children: StatsOptions[], colors?: any }} MultiNormalizedStatsOptions */
/**
* @template {IncomingMessage} Request
* @template {ServerResponse} Response
* @param {import("../index.js").Context<Request, Response>} context
*/
function setupHooks(context) {
function invalid() {
if (context.state) {
context.logger.log("Compilation starting...");
} // We are now in invalid state
// eslint-disable-next-line no-param-reassign
context.state = false; // eslint-disable-next-line no-param-reassign, no-undefined
context.stats = undefined;
} // @ts-ignore
const statsForWebpack4 = webpack.Stats && webpack.Stats.presetToOptions;
/**
* @param {Configuration["stats"]} statsOptions
* @returns {NormalizedStatsOptions}
*/
function normalizeStatsOptions(statsOptions) {
if (statsForWebpack4) {
if (typeof statsOptions === "undefined") {
// eslint-disable-next-line no-param-reassign
statsOptions = {};
} else if (typeof statsOptions === "boolean" || typeof statsOptions === "string") {
// @ts-ignore
// eslint-disable-next-line no-param-reassign
statsOptions = webpack.Stats.presetToOptions(statsOptions);
} // @ts-ignore
return statsOptions;
}
if (typeof statsOptions === "undefined") {
// eslint-disable-next-line no-param-reassign
statsOptions = {
preset: "normal"
};
} else if (typeof statsOptions === "boolean") {
// eslint-disable-next-line no-param-reassign
statsOptions = statsOptions ? {
preset: "normal"
} : {
preset: "none"
};
} else if (typeof statsOptions === "string") {
// eslint-disable-next-line no-param-reassign
statsOptions = {
preset: statsOptions
};
}
return statsOptions;
}
/**
* @param {Stats | MultiStats} stats
*/
function done(stats) {
// We are now on valid state
// eslint-disable-next-line no-param-reassign
context.state = true; // eslint-disable-next-line no-param-reassign
context.stats = stats; // Do the stuff in nextTick, because bundle may be invalidated if a change happened while compiling
process.nextTick(() => {
const {
compiler,
logger,
options,
state,
callbacks
} = context; // Check if still in valid state
if (!state) {
return;
}
logger.log("Compilation finished");
const isMultiCompilerMode = Boolean(
/** @type {MultiCompiler} */
compiler.compilers);
/**
* @type {StatsOptions | MultiStatsOptions | NormalizedStatsOptions | MultiNormalizedStatsOptions}
*/
let statsOptions;
if (typeof options.stats !== "undefined") {
statsOptions = isMultiCompilerMode ? {
children:
/** @type {MultiCompiler} */
compiler.compilers.map(() => options.stats)
} : options.stats;
} else {
statsOptions = isMultiCompilerMode ? {
children:
/** @type {MultiCompiler} */
compiler.compilers.map(child => child.options.stats)
} :
/** @type {Compiler} */
compiler.options.stats;
}
if (isMultiCompilerMode) {
/** @type {MultiNormalizedStatsOptions} */
statsOptions.children =
/** @type {MultiStatsOptions} */
statsOptions.children.map(
/**
* @param {StatsOptions} childStatsOptions
* @return {NormalizedStatsOptions}
*/
childStatsOptions => {
// eslint-disable-next-line no-param-reassign
childStatsOptions = normalizeStatsOptions(childStatsOptions);
if (typeof childStatsOptions.colors === "undefined") {
// eslint-disable-next-line no-param-reassign
childStatsOptions.colors = isColorSupported;
}
return childStatsOptions;
});
} else {
/** @type {NormalizedStatsOptions} */
statsOptions = normalizeStatsOptions(
/** @type {StatsOptions} */
statsOptions);
if (typeof statsOptions.colors === "undefined") {
statsOptions.colors = isColorSupported;
}
} // TODO webpack@4 doesn't support `{ children: [{ colors: true }, { colors: true }] }` for stats
if (
/** @type {MultiCompiler} */
compiler.compilers && statsForWebpack4) {
/** @type {MultiNormalizedStatsOptions} */
statsOptions.colors =
/** @type {MultiNormalizedStatsOptions} */
statsOptions.children.some(
/**
* @param {StatsOptions} child
*/
// @ts-ignore
child => child.colors);
}
const printedStats = stats.toString(statsOptions); // Avoid extra empty line when `stats: 'none'`
if (printedStats) {
// eslint-disable-next-line no-console
console.log(printedStats);
} // eslint-disable-next-line no-param-reassign
context.callbacks = []; // Execute callback that are delayed
callbacks.forEach(
/**
* @param {(...args: any[]) => Stats | MultiStats} callback
*/
callback => {
callback(stats);
});
});
}
context.compiler.hooks.watchRun.tap("webpack-dev-middleware", invalid);
context.compiler.hooks.invalid.tap("webpack-dev-middleware", invalid);
context.compiler.hooks.done.tap("webpack-dev-middleware", done);
}
module.exports = setupHooks;

View File

@ -0,0 +1,58 @@
"use strict";
const path = require("path");
const memfs = require("memfs");
/** @typedef {import("webpack").MultiCompiler} MultiCompiler */
/** @typedef {import("../index.js").IncomingMessage} IncomingMessage */
/** @typedef {import("../index.js").ServerResponse} ServerResponse */
/**
* @template {IncomingMessage} Request
* @template {ServerResponse} Response
* @param {import("../index.js").Context<Request, Response>} context
*/
function setupOutputFileSystem(context) {
let outputFileSystem;
if (context.options.outputFileSystem) {
const {
outputFileSystem: outputFileSystemFromOptions
} = context.options; // Todo remove when we drop webpack@4 support
if (typeof outputFileSystemFromOptions.join !== "function") {
throw new Error("Invalid options: options.outputFileSystem.join() method is expected");
} // Todo remove when we drop webpack@4 support
// @ts-ignore
if (typeof outputFileSystemFromOptions.mkdirp !== "function") {
throw new Error("Invalid options: options.outputFileSystem.mkdirp() method is expected");
}
outputFileSystem = outputFileSystemFromOptions;
} else {
outputFileSystem = memfs.createFsFromVolume(new memfs.Volume()); // TODO: remove when we drop webpack@4 support
// @ts-ignore
outputFileSystem.join = path.join.bind(path);
}
const compilers =
/** @type {MultiCompiler} */
context.compiler.compilers || [context.compiler];
for (const compiler of compilers) {
compiler.outputFileSystem = outputFileSystem;
} // @ts-ignore
// eslint-disable-next-line no-param-reassign
context.outputFileSystem = outputFileSystem;
}
module.exports = setupOutputFileSystem;

View File

@ -0,0 +1,111 @@
"use strict";
const fs = require("fs");
const path = require("path");
/** @typedef {import("webpack").Compiler} Compiler */
/** @typedef {import("webpack").MultiCompiler} MultiCompiler */
/** @typedef {import("webpack").Compilation} Compilation */
/** @typedef {import("../index.js").IncomingMessage} IncomingMessage */
/** @typedef {import("../index.js").ServerResponse} ServerResponse */
/**
* @template {IncomingMessage} Request
* @template {ServerResponse} Response
* @param {import("../index.js").Context<Request, Response>} context
*/
function setupWriteToDisk(context) {
/**
* @type {Compiler[]}
*/
const compilers =
/** @type {MultiCompiler} */
context.compiler.compilers || [context.compiler];
for (const compiler of compilers) {
compiler.hooks.emit.tap("DevMiddleware",
/**
* @param {Compilation} compilation
*/
compilation => {
// @ts-ignore
if (compiler.hasWebpackDevMiddlewareAssetEmittedCallback) {
return;
}
compiler.hooks.assetEmitted.tapAsync("DevMiddleware", (file, info, callback) => {
/**
* @type {string}
*/
let targetPath;
/**
* @type {Buffer}
*/
let content; // webpack@5
if (info.compilation) {
({
targetPath,
content
} = info);
} else {
let targetFile = file;
const queryStringIdx = targetFile.indexOf("?");
if (queryStringIdx >= 0) {
targetFile = targetFile.slice(0, queryStringIdx);
}
let {
outputPath
} = compiler;
outputPath = compilation.getPath(outputPath, {}); // @ts-ignore
content = info;
targetPath = path.join(outputPath, targetFile);
}
const {
writeToDisk: filter
} = context.options;
const allowWrite = filter && typeof filter === "function" ? filter(targetPath) : true;
if (!allowWrite) {
return callback();
}
const dir = path.dirname(targetPath);
const name = compiler.options.name ? `Child "${compiler.options.name}": ` : "";
return fs.mkdir(dir, {
recursive: true
}, mkdirError => {
if (mkdirError) {
context.logger.error(`${name}Unable to write "${dir}" directory to disk:\n${mkdirError}`);
return callback(mkdirError);
}
return fs.writeFile(targetPath, content, writeFileError => {
if (writeFileError) {
context.logger.error(`${name}Unable to write "${targetPath}" asset to disk:\n${writeFileError}`);
return callback(writeFileError);
}
context.logger.log(`${name}Asset written to disk: "${targetPath}"`);
return callback();
});
});
}); // @ts-ignore
compiler.hasWebpackDevMiddlewareAssetEmittedCallback = true;
});
}
}
module.exports = setupWriteToDisk;