first commit

This commit is contained in:
Stefan Hacker
2026-04-03 09:38:48 +02:00
commit 37ad745546
47450 changed files with 3120798 additions and 0 deletions
+308
View File
@@ -0,0 +1,308 @@
"use strict";
/** @typedef {import("../index.js").IncomingMessage} IncomingMessage */
/** @typedef {import("../index.js").ServerResponse} ServerResponse */
/** @typedef {import("../index").OutputFileSystem} OutputFileSystem */
/**
* @typedef {object} ExpectedIncomingMessage
* @property {((name: string) => string | string[] | undefined)=} getHeader get header extra method
* @property {(() => string | undefined)=} getMethod get method extra method
* @property {(() => string | undefined)=} getURL get URL extra method
*/
// eslint-disable-next-line jsdoc/no-restricted-syntax
/**
* @typedef {object} ExpectedServerResponse
* @property {((status: number) => void)=} setStatusCode set status code
* @property {(() => number)=} getStatusCode get status code
* @property {((name: string) => string | string[] | undefined | number)} getHeader get header
* @property {((name: string, value: number | string | Readonly<string[]>) => ExpectedServerResponse)=} setHeader set header
* @property {((name: string) => void)=} removeHeader remove header
* @property {((data: string | Buffer) => void)=} send send
* @property {((data?: string | Buffer) => void)=} finish finish
* @property {(() => string[])=} getResponseHeaders get response header
* @property {(() => boolean)=} getHeadersSent get headers sent
* @property {((data: any) => void)=} stream stream
* @property {(() => any)=} getOutgoing get outgoing
* @property {((name: string, value: any) => void)=} setState set state
*/
/**
* @template {IncomingMessage & ExpectedIncomingMessage} Request
* @param {Request} req req
* @param {string} name name
* @returns {string | string[] | undefined} request header
*/
function getRequestHeader(req, name) {
// Pseudo API
if (typeof req.getHeader === "function") {
return req.getHeader(name);
}
return req.headers[name];
}
/**
* @template {IncomingMessage & ExpectedIncomingMessage} Request
* @param {Request} req req
* @returns {string | undefined} request method
*/
function getRequestMethod(req) {
// Pseudo API
if (typeof req.getMethod === "function") {
return req.getMethod();
}
return req.method;
}
/**
* @template {IncomingMessage & ExpectedIncomingMessage} Request
* @param {Request} req req
* @returns {string | undefined} request URL
*/
function getRequestURL(req) {
// Pseudo API
if (typeof req.getURL === "function") {
return req.getURL();
}
return req.url;
}
/**
* @template {ServerResponse & ExpectedServerResponse} Response
* @param {Response} res res
* @param {number} code code
* @returns {void}
*/
function setStatusCode(res, code) {
// Pseudo API
if (typeof res.setStatusCode === "function") {
res.setStatusCode(code);
return;
}
// Node.js API
res.statusCode = code;
}
/**
* @template {ServerResponse & ExpectedServerResponse} Response
* @param {Response} res res
* @returns {number} status code
*/
function getStatusCode(res) {
// Pseudo API
if (typeof res.getStatusCode === "function") {
return res.getStatusCode();
}
return res.statusCode;
}
/**
* @template {ServerResponse & ExpectedServerResponse} Response
* @param {Response} res res
* @param {string} name name
* @returns {string | string[] | undefined | number} header
*/
function getResponseHeader(res, name) {
// Real and Pseudo API
return res.getHeader(name);
}
/**
* @template {ServerResponse & ExpectedServerResponse} Response
* @param {Response} res res
* @param {string} name name
* @param {number | string | Readonly<string[]>} value value
* @returns {Response} response
*/
function setResponseHeader(res, name, value) {
// Real and Pseudo API
return res.setHeader(name, value);
}
/**
* @template {ServerResponse & ExpectedServerResponse} Response
* @param {Response} res res
* @param {string} name name
* @returns {void}
*/
function removeResponseHeader(res, name) {
// Real and Pseudo API
res.removeHeader(name);
}
/**
* @template {ServerResponse & ExpectedServerResponse} Response
* @param {Response} res res
* @returns {string[]} header names
*/
function getResponseHeaders(res) {
// Pseudo API
if (typeof res.getResponseHeaders === "function") {
return res.getResponseHeaders();
}
return res.getHeaderNames();
}
/**
* @template {ServerResponse & ExpectedServerResponse} Response
* @param {Response} res res
* @returns {boolean} true when headers were sent, otherwise false
*/
function getHeadersSent(res) {
// Pseudo API
if (typeof res.getHeadersSent === "function") {
return res.getHeadersSent();
}
return res.headersSent;
}
/**
* @template {ServerResponse & ExpectedServerResponse} Response
* @param {Response} res res
* @param {import("fs").ReadStream} bufferOrStream buffer or stream
*/
function pipe(res, bufferOrStream) {
// Pseudo API and Koa API
if (typeof res.stream === "function") {
// Writable stream into Readable stream
res.stream(bufferOrStream);
return;
}
// Node.js API and Express API and Hapi API
bufferOrStream.pipe(res);
}
/**
* @template {ServerResponse & ExpectedServerResponse} Response
* @param {Response} res res
* @param {string | Buffer} bufferOrString buffer or string
* @returns {void}
*/
function send(res, bufferOrString) {
// Pseudo API and Express API and Koa API
if (typeof res.send === "function") {
res.send(bufferOrString);
return;
}
res.end(bufferOrString);
}
/**
* @template {ServerResponse & ExpectedServerResponse} Response
* @param {Response} res res
* @param {(string | Buffer)=} data data
*/
function finish(res, data) {
// Pseudo API and Express API and Koa API
if (typeof res.finish === "function") {
res.finish(data);
return;
}
// Pseudo API and Express API and Koa API
res.end(data);
}
/**
* @param {string} filename filename
* @param {OutputFileSystem} outputFileSystem output file system
* @param {number} start start
* @param {number} end end
* @returns {{ bufferOrStream: (Buffer | import("fs").ReadStream), byteLength: number }} result with buffer or stream and byte length
*/
function createReadStreamOrReadFileSync(filename, outputFileSystem, start, end) {
/** @type {string | Buffer | import("fs").ReadStream} */
let bufferOrStream;
/** @type {number} */
let byteLength;
// Stream logic
const isFsSupportsStream = typeof outputFileSystem.createReadStream === "function";
if (isFsSupportsStream) {
bufferOrStream = /** @type {import("fs").createReadStream} */
outputFileSystem.createReadStream(filename, {
start,
end
});
// Handle files with zero bytes
byteLength = end === 0 ? 0 : end - start + 1;
} else {
bufferOrStream = outputFileSystem.readFileSync(filename);
({
byteLength
} = bufferOrStream);
}
return {
bufferOrStream,
byteLength
};
}
/**
* @template {ServerResponse & ExpectedServerResponse} Response
* @param {Response} res res
* @returns {Response} res res
*/
function getOutgoing(res) {
// Pseudo API and Express API and Koa API
if (typeof res.getOutgoing === "function") {
return res.getOutgoing();
}
return res;
}
/**
* @template {ServerResponse & ExpectedServerResponse} Response
* @param {Response} res res
*/
function initState(res) {
if (typeof res.setState === "function") {
return;
}
// fixes #282. credit @cexoso. in certain edge situations res.locals is undefined.
res.locals || (res.locals = {});
}
// eslint-disable-next-line jsdoc/no-restricted-syntax
/**
* @template {ServerResponse & ExpectedServerResponse} Response
* @param {Response} res res
* @param {string} name name
* @param {any} value state
* @returns {void}
*/
function setState(res, name, value) {
if (typeof res.setState === "function") {
res.setState(name, value);
return;
}
// eslint-disable-next-line jsdoc/no-restricted-syntax
/** @type {any} */
res.locals[name] = value;
}
module.exports = {
createReadStreamOrReadFileSync,
finish,
getHeadersSent,
getOutgoing,
getRequestHeader,
getRequestMethod,
getRequestURL,
getResponseHeader,
getResponseHeaders,
getStatusCode,
initState,
pipe,
removeResponseHeader,
send,
setResponseHeader,
setState,
setStatusCode
};
+57
View File
@@ -0,0 +1,57 @@
"use strict";
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:
continue;
}
if (lastIndex !== index) {
// eslint-disable-next-line unicorn/prefer-string-slice
html += str.substring(lastIndex, index);
}
lastIndex = index + 1;
html += escape;
}
// eslint-disable-next-line unicorn/prefer-string-slice
return lastIndex !== index ? html + str.substring(lastIndex, index) : html;
}
module.exports = escapeHtml;
+75
View File
@@ -0,0 +1,75 @@
"use strict";
const crypto = require("node:crypto");
/** @typedef {import("fs").Stats} Stats */
/** @typedef {import("fs").ReadStream} ReadStream */
/**
* Generate a tag for a stat.
* @param {Stats} stats stats
* @returns {{ hash: string, buffer?: Buffer }} etag
*/
function statTag(stats) {
const mtime = stats.mtime.getTime().toString(16);
const size = stats.size.toString(16);
return {
hash: `W/"${size}-${mtime}"`
};
}
/**
* Generate an entity tag.
* @param {Buffer | ReadStream} entity entity
* @returns {Promise<{ hash: string, buffer?: Buffer }>} etag
*/
async function entityTag(entity) {
const sha1 = crypto.createHash("sha1");
if (!Buffer.isBuffer(entity)) {
let byteLength = 0;
/** @type {Buffer[]} */
const buffers = [];
await new Promise((resolve, reject) => {
entity.on("data", chunk => {
sha1.update(chunk);
buffers.push(/** @type {Buffer} */chunk);
byteLength += /** @type {Buffer} */chunk.byteLength;
}).on("end", () => {
resolve(sha1);
}).on("error", reject);
});
return {
buffer: Buffer.concat(buffers),
hash: `"${byteLength.toString(16)}-${sha1.digest("base64").slice(0, 27)}"`
};
}
if (entity.byteLength === 0) {
// Fast-path empty
return {
hash: '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"'
};
}
// Compute hash of entity
const hash = sha1.update(entity).digest("base64").slice(0, 27);
// Compute length of entity
const {
byteLength
} = entity;
return {
hash: `"${byteLength.toString(16)}-${hash}"`
};
}
/**
* Create a simple ETag.
* @param {Buffer | ReadStream | Stats} entity entity
* @returns {Promise<{ hash: string, buffer?: Buffer }>} etag
*/
async function etag(entity) {
const isStrong = Buffer.isBuffer(entity) || typeof (/** @type {ReadStream} */entity.pipe) === "function";
return isStrong ? entityTag(/** @type {Buffer | ReadStream} */entity) : statTag(/** @type {import("fs").Stats} */entity);
}
module.exports = etag;
+140
View File
@@ -0,0 +1,140 @@
"use strict";
const path = require("node:path");
const querystring = require("node:querystring");
// eslint-disable-next-line n/no-deprecated-api
const {
parse
} = require("node:url");
const getPaths = require("./getPaths");
const memorize = require("./memorize");
/** @typedef {import("../index.js").IncomingMessage} IncomingMessage */
/** @typedef {import("../index.js").ServerResponse} ServerResponse */
/**
* @param {string} input input
* @returns {string} unescape input
*/
function decode(input) {
return querystring.unescape(input);
}
const memoizedParse = memorize(parse, undefined, value => {
if (value.pathname) {
value.pathname = decode(value.pathname);
}
return value;
});
const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
/**
* @typedef {object} Extra
* @property {import("fs").Stats=} stats stats
* @property {number=} errorCode error code
* @property {boolean=} immutable true when immutable, otherwise false
*/
/**
* decodeURIComponent.
*
* Allows V8 to only deoptimize this fn instead of all of send().
* @param {string} input
* @returns {string}
*/
// 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
/**
* @template {IncomingMessage} Request
* @template {ServerResponse} Response
* @param {import("../index.js").FilledContext<Request, Response>} context context
* @param {string} url url
* @param {Extra=} extra extra
* @returns {string | undefined} filename
*/
function getFilenameFromUrl(context, url, extra = {}) {
const {
options
} = context;
const paths = getPaths(context);
/** @type {string | undefined} */
let foundFilename;
/** @type {import("node:url").Url} */
let urlObject;
try {
// The `url` property of the `request` is contains only `pathname`, `search` and `hash`
urlObject = memoizedParse(url, false, true);
} catch {
return;
}
for (const {
publicPath,
outputPath,
assetsInfo
} of paths) {
/** @type {string | undefined} */
let filename;
/** @type {import("node:url").Url} */
let publicPathObject;
try {
publicPathObject = memoizedParse(publicPath !== "auto" && publicPath ? publicPath : "/", false, true);
} catch {
continue;
}
const {
pathname
} = urlObject;
const {
pathname: publicPathPathname
} = publicPathObject;
if (pathname && publicPathPathname && pathname.startsWith(publicPathPathname)) {
// Null byte(s)
if (pathname.includes("\0")) {
extra.errorCode = 400;
return;
}
// ".." is malicious
if (UP_PATH_REGEXP.test(path.normalize(`./${pathname}`))) {
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 {
extra.stats = context.outputFileSystem.statSync(filename);
} catch {
continue;
}
if (extra.stats.isFile()) {
foundFilename = filename;
// Rspack does not yet support `assetsInfo`, so we need to check if `assetsInfo` exists here
if (assetsInfo) {
const assetInfo = assetsInfo.get(pathname.slice(publicPathPathname.length));
extra.immutable = assetInfo ? assetInfo.immutable : false;
}
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 {
extra.stats = context.outputFileSystem.statSync(filename);
} catch {
continue;
}
if (extra.stats.isFile()) {
foundFilename = filename;
break;
}
}
}
}
return foundFilename;
}
module.exports = getFilenameFromUrl;
+45
View File
@@ -0,0 +1,45 @@
"use strict";
/** @typedef {import("webpack").Compiler} Compiler */
/** @typedef {import("webpack").Stats} Stats */
/** @typedef {import("webpack").MultiStats} MultiStats */
/** @typedef {import("webpack").Asset} Asset */
/** @typedef {import("../index.js").IncomingMessage} IncomingMessage */
/** @typedef {import("../index.js").ServerResponse} ServerResponse */
/**
* @template {IncomingMessage} Request
* @template {ServerResponse} Response
* @param {import("../index.js").FilledContext<Request, Response>} context context
* @returns {{ outputPath: string, publicPath: string, assetsInfo: Asset["info"] }[]} paths
*/
function getPaths(context) {
const {
stats,
options
} = context;
/* eslint-disable unicorn/prefer-logical-operator-over-ternary */
/** @type {Stats[]} */
const childStats = /** @type {MultiStats} */
stats.stats ? /** @type {MultiStats} */stats.stats : [(/** @type {Stats} */stats)];
/** @type {{ outputPath: string, publicPath: string, assetsInfo: Asset["info"] }[]} */
const publicPaths = [];
for (const {
compilation
} of childStats) {
if (compilation.options.devServer === false) {
continue;
}
// 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,
assetsInfo: compilation.assetsInfo
});
}
return publicPaths;
}
module.exports = getPaths;
+46
View File
@@ -0,0 +1,46 @@
"use strict";
const cacheStore = new WeakMap();
// eslint-disable-next-line jsdoc/no-restricted-syntax
/**
* @template T
* @typedef {(...args: any) => T} FunctionReturning
*/
/**
* @template T
* @param {FunctionReturning<T>} fn memorized function
* @param {({ cache?: Map<string, { data: T }> } | undefined)=} cache cache
* @param {((value: T) => T)=} callback callback
* @returns {FunctionReturning<T>} new function
*/
function memorize(fn, {
cache = new Map()
} = {}, callback = undefined) {
// eslint-disable-next-line jsdoc/no-restricted-syntax
/**
* @param {any} arguments_ args
* @returns {any} result
*/
const memoized = (...arguments_) => {
const [key] = arguments_;
const cacheItem = cache.get(key);
if (cacheItem) {
return cacheItem.data;
}
// @ts-expect-error
let result = fn.apply(this, arguments_);
if (callback) {
result = callback(result);
}
cache.set(key, {
data: result
});
return result;
};
cacheStore.set(memoized, cache);
return memoized;
}
module.exports = memorize;
+41
View File
@@ -0,0 +1,41 @@
"use strict";
/**
* Parse a HTTP token list.
* @param {string} str str
* @returns {string[]} tokens
*/
function parseTokenList(str) {
let end = 0;
let start = 0;
const list = [];
// gather tokens
for (let i = 0, len = str.length; i < len; i++) {
switch (str.charCodeAt(i)) {
case 0x20 /* */:
if (start === end) {
end = i + 1;
start = end;
}
break;
case 0x2c /* , */:
if (start !== end) {
list.push(str.slice(start, end));
}
end = i + 1;
start = end;
break;
default:
end = i + 1;
break;
}
}
// final token
if (start !== end) {
list.push(str.slice(start, end));
}
return list;
}
module.exports = parseTokenList;
+24
View File
@@ -0,0 +1,24 @@
"use strict";
/** @typedef {import("../index.js").IncomingMessage} IncomingMessage */
/** @typedef {import("../index.js").ServerResponse} ServerResponse */
/** @typedef {import("../index.js").Callback} Callback */
/**
* @template {IncomingMessage} Request
* @template {ServerResponse} Response
* @param {import("../index.js").FilledContext<Request, Response>} context context
* @param {Callback} callback callback
* @param {Request=} req 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;
+153
View File
@@ -0,0 +1,153 @@
"use strict";
/** @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>} StatsObjectOptions */
/**
* @template {IncomingMessage} Request
* @template {ServerResponse} Response
* @param {import("../index.js").WithOptional<import("../index.js").Context<Request, Response>, "watching" | "outputFileSystem">} context context
*/
function setupHooks(context) {
/**
* @returns {void}
*/
function invalid() {
if (context.state) {
context.logger.log("Compilation starting...");
}
// We are now in invalid state
context.state = false;
context.stats = undefined;
}
/**
* @param {StatsOptions} statsOptions stats options
* @returns {StatsObjectOptions} object stats options
*/
function normalizeStatsOptions(statsOptions) {
if (typeof statsOptions === "undefined") {
statsOptions = {
preset: "normal"
};
} else if (typeof statsOptions === "boolean") {
statsOptions = statsOptions ? {
preset: "normal"
} : {
preset: "none"
};
} else if (typeof statsOptions === "string") {
statsOptions = {
preset: statsOptions
};
}
return statsOptions;
}
/**
* @param {Stats | MultiStats} stats stats
*/
function done(stats) {
// We are now on valid state
context.state = true;
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 | undefined}
*/
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 {MultiStatsOptions} */
statsOptions.children = /** @type {MultiStatsOptions} */
statsOptions.children.map(
/**
* @param {StatsOptions} childStatsOptions child stats options
* @returns {StatsObjectOptions} object child stats options
*/
childStatsOptions => {
childStatsOptions = normalizeStatsOptions(childStatsOptions);
if (typeof childStatsOptions.colors === "undefined") {
const [firstCompiler] = /** @type {MultiCompiler} */
compiler.compilers;
// TODO remove `colorette` and set minimum supported webpack version is `5.101.0`
childStatsOptions.colors = typeof firstCompiler.webpack !== "undefined" && typeof firstCompiler.webpack.cli !== "undefined" && typeof firstCompiler.webpack.cli.isColorSupported === "function" ? firstCompiler.webpack.cli.isColorSupported() : require("colorette").isColorSupported;
}
return childStatsOptions;
});
} else {
statsOptions = normalizeStatsOptions(/** @type {StatsOptions} */statsOptions);
if (typeof statsOptions.colors === "undefined") {
const {
compiler
} = /** @type {{ compiler: Compiler }} */context;
// TODO remove `colorette` and set minimum supported webpack version is `5.101.0`
statsOptions.colors = typeof compiler.webpack !== "undefined" && typeof compiler.webpack.cli !== "undefined" && typeof compiler.webpack.cli.isColorSupported === "function" ? compiler.webpack.cli.isColorSupported() : require("colorette").isColorSupported;
}
}
const printedStats = stats.toString(/** @type {StatsObjectOptions} */
statsOptions);
// Avoid extra empty line when `stats: 'none'`
if (printedStats) {
// eslint-disable-next-line no-console
console.log(printedStats);
}
context.callbacks = [];
// Execute callback that are delayed
for (const callback of callbacks) {
callback(stats);
}
});
}
// eslint-disable-next-line prefer-destructuring
const compiler = /** @type {import("../index.js").Context<Request, Response>} */
context.compiler;
compiler.hooks.watchRun.tap("webpack-dev-middleware", invalid);
compiler.hooks.invalid.tap("webpack-dev-middleware", invalid);
compiler.hooks.done.tap("webpack-dev-middleware", done);
}
module.exports = setupHooks;
@@ -0,0 +1,57 @@
"use strict";
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").WithOptional<import("../index.js").Context<Request, Response>, "watching" | "outputFileSystem">} context context
*/
function setupOutputFileSystem(context) {
let outputFileSystem;
if (context.options.outputFileSystem) {
const {
outputFileSystem: outputFileSystemFromOptions
} = context.options;
outputFileSystem = outputFileSystemFromOptions;
}
// Don't use `memfs` when developer wants to write everything to a disk, because it doesn't make sense.
else if (context.options.writeToDisk !== true) {
outputFileSystem = memfs.createFsFromVolume(new memfs.Volume());
} else {
const isMultiCompiler = /** @type {MultiCompiler} */
context.compiler.compilers;
if (isMultiCompiler) {
// Prefer compiler with `devServer` option or fallback on the first
// TODO we need to support webpack-dev-server as a plugin or revisit it
const compiler = /** @type {MultiCompiler} */
context.compiler.compilers.find(item => Object.hasOwn(item.options, "devServer") && item.options.devServer !== false);
({
outputFileSystem
} = compiler || /** @type {MultiCompiler} */
context.compiler.compilers[0]);
} else {
({
outputFileSystem
} = context.compiler);
}
}
const compilers = /** @type {MultiCompiler} */
context.compiler.compilers || [context.compiler];
for (const compiler of compilers) {
if (compiler.options.devServer === false) {
continue;
}
// @ts-expect-error
compiler.outputFileSystem = outputFileSystem;
}
// @ts-expect-error
context.outputFileSystem = outputFileSystem;
}
module.exports = setupOutputFileSystem;
+69
View File
@@ -0,0 +1,69 @@
"use strict";
const fs = require("node:fs");
const path = require("node: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").WithOptional<import("../index.js").Context<Request, Response>, "watching" | "outputFileSystem">} context context
*/
function setupWriteToDisk(context) {
/**
* @type {Compiler[]}
*/
const compilers = /** @type {MultiCompiler} */
context.compiler.compilers || [context.compiler];
for (const compiler of compilers) {
if (compiler.options.devServer === false) {
continue;
}
compiler.hooks.emit.tap("DevMiddleware", () => {
// @ts-expect-error
if (compiler.hasWebpackDevMiddlewareAssetEmittedCallback) {
return;
}
compiler.hooks.assetEmitted.tapAsync("DevMiddleware", (file, info, callback) => {
const {
targetPath,
content
} = info;
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-expect-error
compiler.hasWebpackDevMiddlewareAssetEmittedCallback = true;
});
}
}
module.exports = setupWriteToDisk;