From 9aa5c7c90d80b5c80c94c889d5e730f6ab4792e9 Mon Sep 17 00:00:00 2001 From: chenjiahan Date: Thu, 29 Aug 2024 22:00:53 +0800 Subject: [PATCH] refactor: TS --- src/helper.ts | 52 ++++ src/index.js | 266 ---------------- src/index.ts | 227 ++++++++++++++ src/{middleware.js => middleware.ts} | 285 ++++++------------ src/utils/{escapeHtml.js => escapeHtml.ts} | 8 +- ...lenameFromUrl.js => getFilenameFromUrl.ts} | 88 +++--- src/utils/getPaths.js | 39 --- src/utils/getPaths.ts | 32 ++ src/utils/memorize.js | 42 --- src/utils/memorize.ts | 33 ++ .../{parseTokenList.js => parseTokenList.ts} | 11 +- src/utils/ready.js | 26 -- src/utils/ready.ts | 20 ++ src/utils/setupHooks.js | 77 ----- src/utils/setupHooks.ts | 66 ++++ src/utils/setupOutputFileSystem.js | 55 ---- src/utils/setupOutputFileSystem.ts | 51 ++++ ...etupWriteToDisk.js => setupWriteToDisk.ts} | 51 ++-- 18 files changed, 635 insertions(+), 794 deletions(-) create mode 100644 src/helper.ts delete mode 100644 src/index.js create mode 100644 src/index.ts rename src/{middleware.js => middleware.ts} (65%) rename src/utils/{escapeHtml.js => escapeHtml.ts} (92%) rename src/utils/{getFilenameFromUrl.js => getFilenameFromUrl.ts} (65%) delete mode 100644 src/utils/getPaths.js create mode 100644 src/utils/getPaths.ts delete mode 100644 src/utils/memorize.js create mode 100644 src/utils/memorize.ts rename src/utils/{parseTokenList.js => parseTokenList.ts} (81%) delete mode 100644 src/utils/ready.js create mode 100644 src/utils/ready.ts delete mode 100644 src/utils/setupHooks.js create mode 100644 src/utils/setupHooks.ts delete mode 100644 src/utils/setupOutputFileSystem.js create mode 100644 src/utils/setupOutputFileSystem.ts rename src/utils/{setupWriteToDisk.js => setupWriteToDisk.ts} (60%) diff --git a/src/helper.ts b/src/helper.ts new file mode 100644 index 0000000..054c5c4 --- /dev/null +++ b/src/helper.ts @@ -0,0 +1,52 @@ +import { Stats } from "fs"; +import { ReadStream } from "fs"; +import mrmime from "mrmime"; + +/** + * Create a simple ETag. + */ +export async function getEtag(stat: Stats): Promise { + const mtime = stat.mtime.getTime().toString(16); + const size = stat.size.toString(16); + + return `W/"${size}-${mtime}"`; +} + +export function createReadStreamOrReadFileSync( + filename: string, + outputFileSystem: { + createReadStream: ( + path: string, + options: { start: number; end: number }, + ) => Buffer | ReadStream; + }, + start: number, + end: number, +): { bufferOrStream: Buffer | ReadStream; byteLength: number } { + const bufferOrStream = outputFileSystem.createReadStream(filename, { + start, + end, + }); + // Handle files with zero bytes + const byteLength = end === 0 ? 0 : end - start + 1; + + return { bufferOrStream, byteLength }; +} + +/** + * Create a full Content-Type header given a MIME type or extension. + */ +export function getContentType(str: string): false | string { + let mime = mrmime.lookup(str); + if (!mime) { + return false; + } + if ( + mime.startsWith("text/") || + mime === "application/json" || + mime === "application/manifest+json" + ) { + mime += `; charset=utf-8`; + } + return mime; +} diff --git a/src/index.js b/src/index.js deleted file mode 100644 index f3d65e1..0000000 --- a/src/index.js +++ /dev/null @@ -1,266 +0,0 @@ -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 noop = () => {}; - -/** @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 {import("fs").ReadStream} ReadStream */ - -/** - * @typedef {Object} ExtendedServerResponse - * @property {{ webpack?: { devMiddleware?: Context } }} [locals] - */ - -/** @typedef {import("http").IncomingMessage} IncomingMessage */ -/** @typedef {import("http").ServerResponse & ExtendedServerResponse} ServerResponse */ - -/** - * @callback NextFunction - * @param {any} [err] - * @return {void} - */ - -/** - * @typedef {NonNullable} WatchOptions - */ - -/** - * @typedef {Compiler["watching"]} Watching - */ - -/** - * @typedef {ReturnType} MultiWatching - */ - -// TODO fix me after the next webpack release -/** - * @typedef {Object & { createReadStream?: import("fs").createReadStream, statSync?: import("fs").statSync, lstat?: import("fs").lstat, readFileSync?: import("fs").readFileSync }} OutputFileSystem - */ - -/** @typedef {ReturnType} Logger */ - -/** - * @callback Callback - * @param {Stats | MultiStats} [stats] - */ - -/** - * @typedef {Object} ResponseData - * @property {Buffer | ReadStream} data - * @property {number} byteLength - */ - -/** - * @template {IncomingMessage} [RequestInternal=IncomingMessage] - * @template {ServerResponse} [ResponseInternal=ServerResponse] - * @param {RequestInternal} req - * @param {ResponseInternal} res - * @param {Buffer | ReadStream} data - * @param {number} byteLength - * @return {ResponseData} - */ - -/** - * @template {IncomingMessage} [RequestInternal=IncomingMessage] - * @template {ServerResponse} [ResponseInternal=ServerResponse] - * @typedef {Object} Context - * @property {boolean} state - * @property {Stats | MultiStats | undefined} stats - * @property {Callback[]} callbacks - * @property {Options} options - * @property {Compiler | MultiCompiler} compiler - * @property {Watching | MultiWatching | undefined} watching - * @property {Logger} logger - * @property {OutputFileSystem} outputFileSystem - */ - -/** - * @template {IncomingMessage} [RequestInternal=IncomingMessage] - * @template {ServerResponse} [ResponseInternal=ServerResponse] - * @typedef {WithoutUndefined, "watching">} FilledContext - */ - -/** @typedef {Record | Array<{ key: string, value: number | string }>} NormalizedHeaders */ - -/** - * @template {IncomingMessage} [RequestInternal=IncomingMessage] - * @template {ServerResponse} [ResponseInternal=ServerResponse] - * @typedef {NormalizedHeaders | ((req: RequestInternal, res: ResponseInternal, context: Context) => void | undefined | NormalizedHeaders) | undefined} Headers - */ - -/** - * @template {IncomingMessage} [RequestInternal = IncomingMessage] - * @template {ServerResponse} [ResponseInternal = ServerResponse] - * @typedef {Object} Options - * @property {boolean | ((targetPath: string) => boolean)} [writeToDisk] - * @property {NonNullable["publicPath"]} [publicPath] - * @property {boolean | string} [index] - * @property {boolean} [lastModified] - */ - -/** - * @template {IncomingMessage} [RequestInternal=IncomingMessage] - * @template {ServerResponse} [ResponseInternal=ServerResponse] - * @callback Middleware - * @param {RequestInternal} req - * @param {ResponseInternal} res - * @param {NextFunction} next - * @return {Promise} - */ - -/** @typedef {import("./utils/getFilenameFromUrl").Extra} Extra */ - -/** - * @callback GetFilenameFromUrl - * @param {string} url - * @param {Extra=} extra - * @returns {string | undefined} - */ - -/** - * @callback WaitUntilValid - * @param {Callback} callback - */ - -/** - * @callback Invalidate - * @param {Callback} callback - */ - -/** - * @callback Close - * @param {(err: Error | null | undefined) => void} callback - */ - -/** - * @template {IncomingMessage} RequestInternal - * @template {ServerResponse} ResponseInternal - * @typedef {Object} AdditionalMethods - * @property {GetFilenameFromUrl} getFilenameFromUrl - * @property {WaitUntilValid} waitUntilValid - * @property {Invalidate} invalidate - * @property {Close} close - * @property {Context} context - */ - -/** - * @template {IncomingMessage} [RequestInternal=IncomingMessage] - * @template {ServerResponse} [ResponseInternal=ServerResponse] - * @typedef {Middleware & AdditionalMethods} API - */ - -/** - * @template T - * @template {keyof T} K - * @typedef {Omit & Partial} WithOptional - */ - -/** - * @template T - * @template {keyof T} K - * @typedef {T & { [P in K]: NonNullable }} WithoutUndefined - */ - -/** - * @template {IncomingMessage} [RequestInternal=IncomingMessage] - * @template {ServerResponse} [ResponseInternal=ServerResponse] - * @param {Compiler | MultiCompiler} compiler - * @param {Options} [options] - * @returns {API} - */ -function wdm(compiler, options = {}) { - /** - * @type {WithOptional, "watching" | "outputFileSystem">} - */ - const context = { - state: false, - // eslint-disable-next-line no-undefined - stats: undefined, - callbacks: [], - options, - compiler, - logger: compiler.getInfrastructureLogger("webpack-dev-middleware"), - }; - - 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 { - /** - * @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) - ) { - const compiler = /** @type {MultiCompiler} */ (context.compiler); - const watchOptions = compiler.compilers.map( - (childCompiler) => childCompiler.options.watchOptions || {}, - ); - - context.watching = compiler.watch(watchOptions, errorHandler); - } else { - const compiler = /** @type {Compiler} */ (context.compiler); - const watchOptions = compiler.options.watchOptions || {}; - - context.watching = compiler.watch(watchOptions, errorHandler); - } - } - - const filledContext = - /** @type {FilledContext} */ - (context); - - const instance = - /** @type {API} */ - (middleware(filledContext)); - - // API - instance.getFilenameFromUrl = (url, extra) => - getFilenameFromUrl(filledContext, url, extra); - - instance.waitUntilValid = (callback = noop) => { - ready(filledContext, callback); - }; - - instance.invalidate = (callback = noop) => { - ready(filledContext, callback); - - filledContext.watching.invalidate(); - }; - - instance.close = (callback = noop) => { - filledContext.watching.close(callback); - }; - - instance.context = filledContext; - - return instance; -} - -module.exports = wdm; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..73292f8 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,227 @@ +import { + IncomingMessage as HttpIncomingMessage, + ServerResponse as HttpServerResponse, +} from "http"; +import { + Compiler, + MultiCompiler, + Configuration, + Stats, + MultiStats, +} from "webpack"; +import { + createReadStream, + statSync, + lstat, + readFileSync, + ReadStream, +} from "fs"; + +import middleware from "./middleware"; +import { getFilenameFromUrl } from "./utils/getFilenameFromUrl"; +import { setupHooks } from "./utils/setupHooks"; +import { setupWriteToDisk } from "./utils/setupWriteToDisk"; +import { setupOutputFileSystem } from "./utils/setupOutputFileSystem"; +import { ready } from "./utils/ready"; + +const noop = () => {}; + +export type IncomingMessage = HttpIncomingMessage; +export type ServerResponse = HttpServerResponse & { + locals?: { + webpack?: { + devMiddleware?: Context; + }; + }; +}; + +export type NextFunction = (err?: any) => void; + +type WatchOptions = NonNullable; + +type Watching = Compiler["watching"]; +type MultiWatching = ReturnType; + +type OutputFileSystem = { + createReadStream?: typeof createReadStream; + statSync?: typeof statSync; + lstat?: typeof lstat; + readFileSync?: typeof readFileSync; +} & Record; + +type Logger = ReturnType; + +type Callback = (stats?: Stats | MultiStats) => void; + +interface ResponseData { + data: Buffer | ReadStream; + byteLength: number; +} + +export interface Context< + RequestInternal = IncomingMessage, + ResponseInternal = ServerResponse, +> { + state: boolean; + stats?: Stats | MultiStats; + callbacks: Callback[]; + options: Options; + compiler: Compiler | MultiCompiler; + watching?: Watching | MultiWatching; + logger: Logger; + outputFileSystem: OutputFileSystem; +} + +export type FilledContext< + RequestInternal = IncomingMessage, + ResponseInternal = ServerResponse, +> = WithoutUndefined, "watching">; + +export type NormalizedHeaders = + | Record + | Array<{ key: string; value: number | string }>; + +type Headers< + RequestInternal = IncomingMessage, + ResponseInternal = ServerResponse, +> = + | NormalizedHeaders + | (( + req: RequestInternal, + res: ResponseInternal, + context: Context, + ) => void | undefined | NormalizedHeaders) + | undefined; + +interface Options< + RequestInternal = IncomingMessage, + ResponseInternal = ServerResponse, +> { + writeToDisk?: boolean | ((targetPath: string) => boolean); + publicPath?: NonNullable["publicPath"]; + index?: boolean | string; + lastModified?: boolean; +} + +export type Middleware< + RequestInternal = IncomingMessage, + ResponseInternal = ServerResponse, +> = ( + req: RequestInternal, + res: ResponseInternal, + next: NextFunction, +) => Promise; + +type Extra = import("./utils/getFilenameFromUrl").Extra; + +type GetFilenameFromUrl = (url: string, extra?: Extra) => string | undefined; + +type WaitUntilValid = (callback: Callback) => void; +type Invalidate = (callback: Callback) => void; +type Close = (callback: (err: Error | null | undefined) => void) => void; + +interface AdditionalMethods< + RequestInternal = IncomingMessage, + ResponseInternal = ServerResponse, +> { + getFilenameFromUrl: GetFilenameFromUrl; + waitUntilValid: WaitUntilValid; + invalidate: Invalidate; + close: Close; + context: Context; +} + +type API< + RequestInternal = IncomingMessage, + ResponseInternal = ServerResponse, +> = Middleware & + AdditionalMethods; + +export type WithOptional = Omit & Partial; + +type WithoutUndefined = T & { + [P in K]: NonNullable; +}; + +function wdm< + RequestInternal = IncomingMessage, + ResponseInternal = ServerResponse, +>( + compiler: Compiler | MultiCompiler, + options: Options = {}, +): API { + const context: WithOptional< + Context, + "watching" | "outputFileSystem" + > = { + state: false, + stats: undefined, + callbacks: [], + options, + compiler, + logger: compiler.getInfrastructureLogger("webpack-dev-middleware"), + }; + + setupHooks(context); + + if (options.writeToDisk) { + setupWriteToDisk(context); + } + + setupOutputFileSystem(context); + + if ((context.compiler as Compiler).watching) { + context.watching = (context.compiler as Compiler).watching; + } else { + const errorHandler = (error: Error | null | undefined) => { + if (error) { + context.logger.error(error); + } + }; + + if (Array.isArray((context.compiler as MultiCompiler).compilers)) { + const multiCompiler = context.compiler as MultiCompiler; + const watchOptions = multiCompiler.compilers.map( + (childCompiler) => childCompiler.options.watchOptions || {}, + ); + + context.watching = multiCompiler.watch(watchOptions, errorHandler); + } else { + const singleCompiler = context.compiler as Compiler; + const watchOptions = singleCompiler.options.watchOptions || {}; + + context.watching = singleCompiler.watch(watchOptions, errorHandler); + } + } + + const filledContext = context as FilledContext< + RequestInternal, + ResponseInternal + >; + const instance = middleware(filledContext) as API< + RequestInternal, + ResponseInternal + >; + + instance.getFilenameFromUrl = (url, extra) => + getFilenameFromUrl(filledContext, url, extra); + + instance.waitUntilValid = (callback = noop) => { + ready(filledContext, callback); + }; + + instance.invalidate = (callback = noop) => { + ready(filledContext, callback); + filledContext.watching!.invalidate(); + }; + + instance.close = (callback = noop) => { + filledContext.watching!.close(callback); + }; + + instance.context = filledContext; + + return instance; +} + +export default wdm; diff --git a/src/middleware.js b/src/middleware.ts similarity index 65% rename from src/middleware.js rename to src/middleware.ts index c8a5430..76e6d5b 100644 --- a/src/middleware.js +++ b/src/middleware.ts @@ -1,105 +1,44 @@ -const mrmime = require("mrmime"); - -const onFinishedStream = require("on-finished"); - -const getFilenameFromUrl = require("./utils/getFilenameFromUrl"); -const ready = require("./utils/ready"); -const parseTokenList = require("./utils/parseTokenList"); -const memorize = require("./utils/memorize"); - -/** @typedef {import("fs").Stats} Stats */ - -/** - * Create a simple ETag. - * - * @param {Stats} stat - * @return {Promise} - */ -async function getEtag(stat) { - const mtime = stat.mtime.getTime().toString(16); - const size = stat.size.toString(16); - - return `W/"${size}-${mtime}"`; -} - -/** - * @typedef {Object} ExpectedResponse - * @property {(status: number) => void} [status] - * @property {(data: any) => void} [send] - * @property {(data: any) => void} [pipeInto] - */ - -/** - * @param {string} filename - * @param {import("./index").OutputFileSystem} outputFileSystem - * @param {number} start - * @param {number} end - * @returns {{ bufferOrStream: (Buffer | import("fs").ReadStream), byteLength: number }} - */ -function createReadStreamOrReadFileSync( - filename, - outputFileSystem, - start, - end, -) { - const bufferOrStream = - /** @type {import("fs").createReadStream} */ - (outputFileSystem.createReadStream)(filename, { - start, - end, - }); - // Handle files with zero bytes - const byteLength = end === 0 ? 0 : end - start + 1; - - return { bufferOrStream, byteLength }; -} - -/** - * Create a full Content-Type header given a MIME type or extension. - * - * @param {string} str - * @return {false|string} - */ -function getContentType(str) { - let mime = mrmime.lookup(str); - if (!mime) { - return false; - } - if ( - mime.startsWith("text/") || - mime === "application/json" || - mime === "application/manifest+json" - ) { - mime += `; charset=utf-8`; - } - return mime; -} +import onFinishedStream from "on-finished"; +import { getFilenameFromUrl } from "./utils/getFilenameFromUrl"; +import { ready } from "./utils/ready"; +import { parseTokenList } from "./utils/parseTokenList"; +import { memorize } from "./utils/memorize"; +import { Stats, ReadStream } from "fs"; +import { Range, Result, Ranges } from "range-parser"; +import { Context, FilledContext, Middleware } from "./index"; +import { escapeHtml } from "./utils/escapeHtml"; + +type ExpectedResponse = { + status?: (status: number) => void; + send?: (data: any) => void; + pipeInto?: (data: any) => void; +}; -/** @typedef {import("./index.js").NextFunction} NextFunction */ -/** @typedef {import("./index.js").IncomingMessage} IncomingMessage */ -/** @typedef {import("./index.js").ServerResponse} ServerResponse */ -/** @typedef {import("./index.js").NormalizedHeaders} NormalizedHeaders */ -/** @typedef {import("fs").ReadStream} ReadStream */ +type NextFunction = import("./index").NextFunction; +type IncomingMessage = import("./index").IncomingMessage; +type ServerResponse = import("./index").ServerResponse; +type NormalizedHeaders = import("./index").NormalizedHeaders; const BYTES_RANGE_REGEXP = /^ *bytes/i; /** * @param {string} type * @param {number} size - * @param {import("range-parser").Range} [range] + * @param {Range} [range] * @returns {string} */ -function getValueContentRangeHeader(type, size, range) { +function getValueContentRangeHeader( + type: string, + size: number, + range?: Range, +): string { return `${type} ${range ? `${range.start}-${range.end}` : "*"}/${size}`; } /** * Parse an HTTP Date into a number. - * - * @param {string} date - * @returns {number} */ -function parseHttpDate(date) { +function parseHttpDate(date: string): number { const timestamp = date && Date.parse(date); // istanbul ignore next: guard against date.js Date.parse patching @@ -108,31 +47,19 @@ function parseHttpDate(date) { 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) { +function destroyStream(stream: ReadStream, suppress: boolean): void { 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(); - } - }, - ); + stream.on("open", function onOpenClose(this: ReadStream) { + if (typeof (this as any).fd === "number") { + // actually close down the fd + this.close(); + } + }); } if (typeof stream.addListener === "function" && suppress) { @@ -141,8 +68,7 @@ function destroyStream(stream, suppress) { } } -/** @type {Record} */ -const statuses = { +const statuses: Record = { 400: "Bad Request", 403: "Forbidden", 404: "Not Found", @@ -153,37 +79,25 @@ const statuses = { const parseRangeHeaders = memorize( /** * @param {string} value - * @returns {import("range-parser").Result | import("range-parser").Ranges} + * @returns {Result | Ranges} */ - (value) => { + (value: string): Result | Ranges => { 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=} headers headers - */ - -/** - * @template {IncomingMessage} Request - * @template {ServerResponse} Response - * @param {import("./index.js").FilledContext} context - * @return {import("./index.js").Middleware} - */ -function wrapper(context) { +export function wrapper< + Request extends IncomingMessage, + Response extends ServerResponse, +>(context: FilledContext): Middleware { return async function middleware(req, res, next) { const acceptedMethods = ["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 || {}; async function goNext() { @@ -191,9 +105,7 @@ function wrapper(context) { ready( context, () => { - /** @type {any} */ - // eslint-disable-next-line no-param-reassign - (res.locals).webpack = { devMiddleware: context }; + (res as any).locals.webpack = { devMiddleware: context }; resolve(next()); }, @@ -209,13 +121,14 @@ function wrapper(context) { } /** - * @param {number} status status - * @param {Partial>=} options options + * @param {number} status + * @param {Partial>=} options * @returns {void} */ - function sendError(status, options) { - // eslint-disable-next-line global-require - const escapeHtml = require("./utils/escapeHtml"); + function sendError( + status: number, + options?: Partial>, + ): void { const content = statuses[status] || String(status); const document = Buffer.from( ` @@ -307,7 +220,7 @@ function wrapper(context) { // received field-value is not a valid HTTP-date. if (!isNaN(unmodifiedSince)) { const lastModified = parseHttpDate( - /** @type {string} */ (res.getHeader("Last-Modified")), + /** @type {string} */ res.getHeader("Last-Modified"), ); return isNaN(lastModified) || lastModified > unmodifiedSince; @@ -320,7 +233,7 @@ function wrapper(context) { /** * @returns {boolean} is cachable */ - function isCachable() { + function isCachable(): boolean { return ( (res.statusCode >= 200 && res.statusCode < 300) || res.statusCode === 304 @@ -331,7 +244,7 @@ function wrapper(context) { * @param {import("http").OutgoingHttpHeaders} resHeaders * @returns {boolean} */ - function isFresh(resHeaders) { + function isFresh(resHeaders: Record): boolean { // 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"]; @@ -403,10 +316,8 @@ function wrapper(context) { return true; } - function isRangeFresh() { - const ifRange = - /** @type {string | undefined} */ - (req.headers["if-range"]); + function isRangeFresh(): boolean { + const ifRange = req.headers["if-range"]; if (!ifRange) { return true; @@ -414,7 +325,7 @@ function wrapper(context) { // if-range as etag if (ifRange.indexOf('"') !== -1) { - const etag = /** @type {string | undefined} */ (res.getHeader("ETag")); + const etag = res.getHeader("ETag"); if (!etag) { return true; @@ -424,9 +335,7 @@ function wrapper(context) { } // if-range as modified date - const lastModified = - /** @type {string | undefined} */ - (res.getHeader("Last-Modified")); + const lastModified = res.getHeader("Last-Modified"); if (!lastModified) { return true; @@ -435,17 +344,13 @@ function wrapper(context) { return parseHttpDate(lastModified) <= parseHttpDate(ifRange); } - /** - * @returns {string | undefined} - */ - function getRangeHeader() { - const rage = req.headers.range; + function getRangeHeader(): string | undefined { + const range = req.headers.range; - if (rage && BYTES_RANGE_REGEXP.test(rage)) { - return rage; + if (range && BYTES_RANGE_REGEXP.test(range)) { + return range; } - // eslint-disable-next-line no-undefined return undefined; } @@ -453,7 +358,10 @@ function wrapper(context) { * @param {import("range-parser").Range} range * @returns {[number, number]} */ - function getOffsetAndLenFromRange(range) { + function getOffsetAndLenFromRange(range: { + start: number; + end: number; + }): [number, number] { const offset = range.start; const len = range.end - range.start + 1; @@ -465,7 +373,7 @@ function wrapper(context) { * @param {number} len * @returns {[number, number]} */ - function calcStartAndEnd(offset, len) { + function calcStartAndEnd(offset: number, len: number): [number, number] { const start = offset; const end = Math.max(offset, offset + len - 1); @@ -474,13 +382,8 @@ function wrapper(context) { async function processRequest() { // Pipe and SendFile - /** @type {import("./utils/getFilenameFromUrl").Extra} */ - const extra = {}; - const filename = getFilenameFromUrl( - context, - /** @type {string} */ (req.url), - extra, - ); + const extra: { errorCode?: number; stats?: import("fs").Stats } = {}; + const filename = getFilenameFromUrl(context, req.url as string, extra); if (extra.errorCode) { if (extra.errorCode === 403) { @@ -498,7 +401,7 @@ function wrapper(context) { return; } - const { size } = /** @type {import("fs").Stats} */ (extra.stats); + const { size } = extra.stats as import("fs").Stats; let len = size; let offset = 0; @@ -519,9 +422,9 @@ function wrapper(context) { } if (context.options.lastModified && !res.getHeader("Last-Modified")) { - const modified = - /** @type {import("fs").Stats} */ - (extra.stats).mtime.toUTCString(); + const modified = ( + extra.stats as import("fs").Stats + ).mtime.toUTCString(); res.setHeader("Last-Modified", modified); } @@ -529,7 +432,7 @@ function wrapper(context) { const rangeHeader = getRangeHeader(); if (!res.getHeader("ETag")) { - const value = /** @type {import("fs").Stats} */ (extra.stats); + const value = extra.stats as import("fs").Stats; if (value) { const hash = await getEtag(value); res.setHeader("ETag", hash); @@ -552,10 +455,8 @@ function wrapper(context) { if ( isCachable() && isFresh({ - etag: /** @type {string | undefined} */ (res.getHeader("ETag")), - "last-modified": - /** @type {string | undefined} */ - (res.getHeader("Last-Modified")), + etag: res.getHeader("ETag") as string, + "last-modified": res.getHeader("Last-Modified") as string, }) ) { res.statusCode = 304; @@ -573,9 +474,7 @@ function wrapper(context) { } if (rangeHeader) { - let parsedRanges = - /** @type {import("range-parser").Ranges | import("range-parser").Result | []} */ - (parseRangeHeaders(`${size}|${rangeHeader}`)); + let parsedRanges = parseRangeHeaders(`${size}|${rangeHeader}`); // If-Range support if (!isRangeFresh()) { @@ -608,25 +507,24 @@ function wrapper(context) { } if (parsedRanges !== -2 && parsedRanges.length === 1) { - // Content-Range res.statusCode = 206; res.setHeader( "Content-Range", getValueContentRangeHeader( "bytes", size, - /** @type {import("range-parser").Ranges} */ (parsedRanges)[0], + parsedRanges[0] as import("range-parser").Ranges, ), ); - [offset, len] = getOffsetAndLenFromRange(parsedRanges[0]); + [offset, len] = getOffsetAndLenFromRange( + parsedRanges[0] as import("range-parser").Range, + ); } } - /** @type {undefined | Buffer | ReadStream} */ - let bufferOrStream; - /** @type {number} */ - let byteLength; + let bufferOrStream: undefined | Buffer | ReadStream; + let byteLength: number; const [start, end] = calcStartAndEnd(offset, len); @@ -642,11 +540,9 @@ function wrapper(context) { return; } - // @ts-ignore res.setHeader("Content-Length", byteLength); if (req.method === "HEAD") { - // For Koa if (res.statusCode === 404) { res.statusCode = 200; } @@ -656,31 +552,21 @@ function wrapper(context) { } const isPipeSupports = - typeof ( - /** @type {import("fs").ReadStream} */ (bufferOrStream).pipe - ) === "function"; + typeof (bufferOrStream as ReadStream).pipe === "function"; if (!isPipeSupports) { - res.end(/** @type {Buffer} */ (bufferOrStream)); + res.end(bufferOrStream as Buffer); return; } - // Cleanup const cleanup = () => { - destroyStream( - /** @type {import("fs").ReadStream} */ (bufferOrStream), - true, - ); + destroyStream(bufferOrStream as ReadStream, true); }; - // Error handling - /** @type {import("fs").ReadStream} */ - (bufferOrStream).on("error", (error) => { - // clean up stream early + (bufferOrStream as ReadStream).on("error", (error) => { cleanup(); - // Handle Error - switch (/** @type {NodeJS.ErrnoException} */ (error).code) { + switch (error.code) { case "ENAMETOOLONG": case "ENOENT": case "ENOTDIR": @@ -692,9 +578,8 @@ function wrapper(context) { } }); - /** @type {ReadStream} */ (bufferOrStream).pipe(res); + (bufferOrStream as ReadStream).pipe(res); - // Response finished, cleanup onFinishedStream(res, cleanup); } diff --git a/src/utils/escapeHtml.js b/src/utils/escapeHtml.ts similarity index 92% rename from src/utils/escapeHtml.js rename to src/utils/escapeHtml.ts index 4a131ef..1f2ad4b 100644 --- a/src/utils/escapeHtml.js +++ b/src/utils/escapeHtml.ts @@ -4,7 +4,7 @@ const matchHtmlRegExp = /["'&<>]/; * @param {string} string raw HTML * @returns {string} escaped HTML */ -function escapeHtml(string) { +export function escapeHtml(string: string): string { const str = `${string}`; const match = matchHtmlRegExp.exec(str); @@ -12,7 +12,7 @@ function escapeHtml(string) { return str; } - let escape; + let escape: string; let html = ""; let index = 0; let lastIndex = 0; @@ -53,6 +53,4 @@ function escapeHtml(string) { } return lastIndex !== index ? html + str.substring(lastIndex, index) : html; -} - -module.exports = escapeHtml; +} \ No newline at end of file diff --git a/src/utils/getFilenameFromUrl.js b/src/utils/getFilenameFromUrl.ts similarity index 65% rename from src/utils/getFilenameFromUrl.js rename to src/utils/getFilenameFromUrl.ts index 67852ee..ad6278b 100644 --- a/src/utils/getFilenameFromUrl.js +++ b/src/utils/getFilenameFromUrl.ts @@ -1,28 +1,30 @@ -const path = require("path"); -const { parse } = require("url"); -const querystring = require("querystring"); - -const getPaths = require("./getPaths"); -const memorize = require("./memorize"); - -/** @typedef {import("../index.js").IncomingMessage} IncomingMessage */ -/** @typedef {import("../index.js").ServerResponse} ServerResponse */ +import path from "path"; +import { parse, UrlWithStringQuery } from "url"; +import querystring from "querystring"; +import { getPaths } from "./getPaths"; +import { memorize } from "./memorize"; +import { IncomingMessage, ServerResponse, FilledContext } from "../index"; +import { Stats } from "fs"; // eslint-disable-next-line no-undefined -const memoizedParse = memorize(parse, undefined, (value) => { - if (value.pathname) { - // eslint-disable-next-line no-param-reassign - value.pathname = decode(value.pathname); - } +const memoizedParse = memorize( + parse, + undefined, + (value: UrlWithStringQuery) => { + if (value.pathname) { + // eslint-disable-next-line no-param-reassign + value.pathname = decode(value.pathname); + } - return value; -}); + return value; + }, +); const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/; /** * @typedef {Object} Extra - * @property {import("fs").Stats=} stats + * @property {Stats=} stats * @property {number=} errorCode */ @@ -35,28 +37,27 @@ const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/; * @returns {string} */ -function decode(input) { +function decode(input: string): string { return querystring.unescape(input); } +export type Extra = { stats?: Stats; errorCode?: number }; + // 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} context - * @param {string} url - * @param {Extra=} extra - * @returns {string | undefined} - */ -function getFilenameFromUrl(context, url, extra = {}) { +export function getFilenameFromUrl< + Request extends IncomingMessage, + Response extends ServerResponse, +>( + context: FilledContext, + url: string, + extra: Extra = {}, +): string | undefined { const { options } = context; const paths = getPaths(context); - /** @type {string | undefined} */ - let foundFilename; - /** @type {URL} */ - let urlObject; + let foundFilename: string | undefined; + let urlObject: UrlWithStringQuery; try { // The `url` property of the `request` is contains only `pathname`, `search` and `hash` @@ -66,10 +67,8 @@ function getFilenameFromUrl(context, url, extra = {}) { } for (const { publicPath, outputPath } of paths) { - /** @type {string | undefined} */ - let filename; - /** @type {URL} */ - let publicPathObject; + let filename: string | undefined; + let publicPathObject: UrlWithStringQuery; try { publicPathObject = memoizedParse( @@ -85,7 +84,11 @@ function getFilenameFromUrl(context, url, extra = {}) { const { pathname } = urlObject; const { pathname: publicPathPathname } = publicPathObject; - if (pathname && pathname.startsWith(publicPathPathname)) { + if ( + pathname && + publicPathPathname && + pathname.startsWith(publicPathPathname) + ) { // Null byte(s) if (pathname.includes("\0")) { // eslint-disable-next-line no-param-reassign @@ -112,10 +115,7 @@ function getFilenameFromUrl(context, url, extra = {}) { ); try { - // eslint-disable-next-line no-param-reassign - extra.stats = - /** @type {import("fs").statSync} */ - (context.outputFileSystem.statSync)(filename); + extra.stats = context.outputFileSystem.statSync?.(filename) as Stats; } catch (_ignoreError) { // eslint-disable-next-line no-continue continue; @@ -123,7 +123,6 @@ function getFilenameFromUrl(context, url, extra = {}) { if (extra.stats.isFile()) { foundFilename = filename; - break; } else if ( extra.stats.isDirectory() && @@ -138,9 +137,7 @@ function getFilenameFromUrl(context, url, extra = {}) { filename = path.join(filename, indexValue); try { - extra.stats = - /** @type {import("fs").statSync} */ - (context.outputFileSystem.statSync)(filename); + extra.stats = context.outputFileSystem.statSync?.(filename) as Stats; } catch (__ignoreError) { // eslint-disable-next-line no-continue continue; @@ -148,7 +145,6 @@ function getFilenameFromUrl(context, url, extra = {}) { if (extra.stats.isFile()) { foundFilename = filename; - break; } } @@ -158,5 +154,3 @@ function getFilenameFromUrl(context, url, extra = {}) { // eslint-disable-next-line consistent-return return foundFilename; } - -module.exports = getFilenameFromUrl; diff --git a/src/utils/getPaths.js b/src/utils/getPaths.js deleted file mode 100644 index 93dafa8..0000000 --- a/src/utils/getPaths.js +++ /dev/null @@ -1,39 +0,0 @@ -/** @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").FilledContext} 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; diff --git a/src/utils/getPaths.ts b/src/utils/getPaths.ts new file mode 100644 index 0000000..b2a4479 --- /dev/null +++ b/src/utils/getPaths.ts @@ -0,0 +1,32 @@ +import { Stats, MultiStats } from "webpack"; +import { IncomingMessage, ServerResponse, FilledContext } from "../index.js"; + +export function getPaths< + Request extends IncomingMessage, + Response extends ServerResponse, +>( + context: FilledContext, +): { outputPath: string; publicPath: string }[] { + const { stats, options } = context; + const childStats: Stats[] = (stats as MultiStats).stats + ? (stats as MultiStats).stats + : [stats as Stats]; + + const publicPaths: { outputPath: string; publicPath: string }[] = []; + + 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; +} diff --git a/src/utils/memorize.js b/src/utils/memorize.js deleted file mode 100644 index 03bbb1c..0000000 --- a/src/utils/memorize.js +++ /dev/null @@ -1,42 +0,0 @@ -const cacheStore = new WeakMap(); - -/** - * @template T - * @param {Function} fn - * @param {{ cache?: Map } | undefined} cache - * @param {((value: T) => T)=} callback - * @returns {any} - */ -function memorize(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; - } - - // @ts-ignore - 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; diff --git a/src/utils/memorize.ts b/src/utils/memorize.ts new file mode 100644 index 0000000..cd8fefd --- /dev/null +++ b/src/utils/memorize.ts @@ -0,0 +1,33 @@ +const cacheStore = new WeakMap>(); + +export function memorize( + fn: (...args: any[]) => T, + { cache = new Map() } = {}, + callback?: (value: T) => T, +): (...args: any[]) => T { + const memoized = (...args: any[]): T => { + const [key] = args; + const cacheItem = cache.get(key); + + if (cacheItem) { + return cacheItem.data; + } + + // @ts-ignore + let result = fn.apply(this, args); + + if (callback) { + result = callback(result); + } + + cache.set(key, { + data: result, + }); + + return result; + }; + + cacheStore.set(memoized, cache); + + return memoized; +} diff --git a/src/utils/parseTokenList.js b/src/utils/parseTokenList.ts similarity index 81% rename from src/utils/parseTokenList.js rename to src/utils/parseTokenList.ts index fed6c47..c533385 100644 --- a/src/utils/parseTokenList.js +++ b/src/utils/parseTokenList.ts @@ -1,14 +1,11 @@ /** * Parse a HTTP token list. - * - * @param {string} str - * @returns {string[]} tokens */ -function parseTokenList(str) { +export function parseTokenList(str: string): string[] { let end = 0; let start = 0; - const list = []; + const list: string[] = []; // gather tokens for (let i = 0, len = str.length; i < len; i++) { @@ -38,6 +35,4 @@ function parseTokenList(str) { } return list; -} - -module.exports = parseTokenList; +} \ No newline at end of file diff --git a/src/utils/ready.js b/src/utils/ready.js deleted file mode 100644 index 741bcaa..0000000 --- a/src/utils/ready.js +++ /dev/null @@ -1,26 +0,0 @@ -/** @typedef {import("../index.js").IncomingMessage} IncomingMessage */ -/** @typedef {import("../index.js").ServerResponse} ServerResponse */ - -/** - * @template {IncomingMessage} Request - * @template {ServerResponse} Response - * @param {import("../index.js").FilledContext} 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; diff --git a/src/utils/ready.ts b/src/utils/ready.ts new file mode 100644 index 0000000..b73d712 --- /dev/null +++ b/src/utils/ready.ts @@ -0,0 +1,20 @@ +import { IncomingMessage, ServerResponse, FilledContext } from "../index.js"; + +export function ready< + Request extends IncomingMessage, + Response extends ServerResponse, +>( + context: FilledContext, + callback: (...args: any[]) => any, + req?: Request, +): void { + 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); +} diff --git a/src/utils/setupHooks.js b/src/utils/setupHooks.js deleted file mode 100644 index d068979..0000000 --- a/src/utils/setupHooks.js +++ /dev/null @@ -1,77 +0,0 @@ -/** @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} StatsObjectOptions */ - -/** - * @template {IncomingMessage} Request - * @template {ServerResponse} Response - * @param {import("../index.js").WithOptional, "watching" | "outputFileSystem">} 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; - } - - /** - * @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 { logger, state, callbacks } = context; - - // Check if still in valid state - if (!state) { - return; - } - - logger.log("Compilation finished"); - - // 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); - }, - ); - }); - } - - // eslint-disable-next-line prefer-destructuring - const compiler = - /** @type {import("../index.js").Context} */ - (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; diff --git a/src/utils/setupHooks.ts b/src/utils/setupHooks.ts new file mode 100644 index 0000000..067a969 --- /dev/null +++ b/src/utils/setupHooks.ts @@ -0,0 +1,66 @@ +import { + Configuration, + Compiler, + MultiCompiler, + Stats, + MultiStats, +} from "webpack"; +import { + IncomingMessage, + ServerResponse, + WithOptional, + Context, +} from "../index.js"; + +export function setupHooks< + Request extends IncomingMessage, + Response extends ServerResponse, +>( + context: WithOptional< + Context, + "watching" | "outputFileSystem" + >, +): void { + function invalid() { + if (context.state) { + context.logger.log("Compilation starting..."); + } + + // We are now in invalid state + context.state = false; + context.stats = undefined; + } + + /** + * @param {Stats | MultiStats} stats + */ + function done(stats: Stats | MultiStats) { + // 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 { logger, state, callbacks } = context; + + // Check if still in valid state + if (!state) { + return; + } + + logger.log("Compilation finished"); + + context.callbacks = []; + + // Execute callback that are delayed + callbacks.forEach((callback) => { + callback(stats); + }); + }); + } + + const compiler = context.compiler as 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); +} diff --git a/src/utils/setupOutputFileSystem.js b/src/utils/setupOutputFileSystem.js deleted file mode 100644 index 22a82b1..0000000 --- a/src/utils/setupOutputFileSystem.js +++ /dev/null @@ -1,55 +0,0 @@ -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, "watching" | "outputFileSystem">} context - */ -function setupOutputFileSystem(context) { - let outputFileSystem; - - // Don't use `memfs` when developer wants to write everything to a disk, because it doesn't make sense. - 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.filter((item) => - Object.prototype.hasOwnProperty.call(item.options, "devServer"), - ); - - ({ outputFileSystem } = - compiler[0] || - /** @type {MultiCompiler} */ - (context.compiler).compilers[0]); - } else { - ({ outputFileSystem } = context.compiler); - } - } - - const compilers = - /** @type {MultiCompiler} */ - (context.compiler).compilers || [context.compiler]; - - for (const compiler of compilers) { - // @ts-ignore - compiler.outputFileSystem = outputFileSystem; - } - - // @ts-ignore - // eslint-disable-next-line no-param-reassign - context.outputFileSystem = outputFileSystem; -} - -module.exports = setupOutputFileSystem; diff --git a/src/utils/setupOutputFileSystem.ts b/src/utils/setupOutputFileSystem.ts new file mode 100644 index 0000000..34ccd31 --- /dev/null +++ b/src/utils/setupOutputFileSystem.ts @@ -0,0 +1,51 @@ +import { createFsFromVolume, Volume } from "memfs"; +import { MultiCompiler } from "webpack"; +import { + IncomingMessage, + ServerResponse, + WithOptional, + Context, +} from "../index"; + +export function setupOutputFileSystem< + Request extends IncomingMessage, + Response extends ServerResponse, +>( + context: WithOptional< + Context, + "watching" | "outputFileSystem" + >, +): void { + let outputFileSystem: any; + + // Don't use `memfs` when developer wants to write everything to a disk, because it doesn't make sense. + if (context.options.writeToDisk !== true) { + outputFileSystem = createFsFromVolume(new Volume()); + } else { + const isMultiCompiler = (context.compiler as MultiCompiler).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 = (context.compiler as MultiCompiler).compilers.filter( + (item) => + Object.prototype.hasOwnProperty.call(item.options, "devServer"), + ); + + ({ outputFileSystem } = + compiler[0] || (context.compiler as MultiCompiler).compilers[0]); + } else { + ({ outputFileSystem } = context.compiler); + } + } + + const compilers = (context.compiler as MultiCompiler).compilers || [ + context.compiler, + ]; + + for (const compiler of compilers) { + compiler.outputFileSystem = outputFileSystem; + } + + context.outputFileSystem = outputFileSystem; +} diff --git a/src/utils/setupWriteToDisk.js b/src/utils/setupWriteToDisk.ts similarity index 60% rename from src/utils/setupWriteToDisk.js rename to src/utils/setupWriteToDisk.ts index e70becf..c973195 100644 --- a/src/utils/setupWriteToDisk.js +++ b/src/utils/setupWriteToDisk.ts @@ -1,29 +1,28 @@ -const fs = require("fs"); -const path = require("path"); +import fs from "fs"; +import path from "path"; +import { Compiler, MultiCompiler } from "webpack"; +import { + IncomingMessage, + ServerResponse, + WithOptional, + Context, +} from "../index"; -/** @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, "watching" | "outputFileSystem">} context - */ -function setupWriteToDisk(context) { - /** - * @type {Compiler[]} - */ - const compilers = - /** @type {MultiCompiler} */ - (context.compiler).compilers || [context.compiler]; +export function setupWriteToDisk< + Request extends IncomingMessage, + Response extends ServerResponse, +>( + context: WithOptional< + Context, + "watching" | "outputFileSystem" + >, +): void { + const compilers: Compiler[] = (context.compiler as MultiCompiler) + .compilers || [context.compiler as Compiler]; for (const compiler of compilers) { compiler.hooks.emit.tap("DevMiddleware", () => { - // @ts-ignore - if (compiler.hasWebpackDevMiddlewareAssetEmittedCallback) { + if ((compiler as any).hasWebpackDevMiddlewareAssetEmittedCallback) { return; } @@ -49,7 +48,6 @@ function setupWriteToDisk(context) { context.logger.error( `${name}Unable to write "${dir}" directory to disk:\n${mkdirError}`, ); - return callback(mkdirError); } @@ -58,24 +56,19 @@ function setupWriteToDisk(context) { 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; + (compiler as any).hasWebpackDevMiddlewareAssetEmittedCallback = true; }); } } - -module.exports = setupWriteToDisk;