123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630 |
- "use strict";
- const mime = require("mime-types");
- const {
- validate
- } = require("schema-utils");
- const middleware = require("./middleware");
- const schema = require("./options.json");
- const getFilenameFromUrl = require("./utils/getFilenameFromUrl");
- const ready = require("./utils/ready");
- const setupHooks = require("./utils/setupHooks");
- const setupOutputFileSystem = require("./utils/setupOutputFileSystem");
- const setupWriteToDisk = require("./utils/setupWriteToDisk");
- 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 {import("fs").ReadStream} ReadStream */
- /**
- * @typedef {object} ExtendedServerResponse
- * @property {{ webpack?: { devMiddleware?: Context<IncomingMessage, ServerResponse> } }=} locals locals
- */
- /** @typedef {import("http").IncomingMessage} IncomingMessage */
- /** @typedef {import("http").ServerResponse & ExtendedServerResponse} ServerResponse */
- // eslint-disable-next-line jsdoc/no-restricted-syntax
- /**
- * @callback NextFunction
- * @param {any=} err error
- * @returns {void}
- */
- /**
- * @typedef {NonNullable<Configuration["watchOptions"]>} WatchOptions
- */
- /**
- * @typedef {Compiler["watching"]} Watching
- */
- /**
- * @typedef {ReturnType<MultiCompiler["watch"]>} MultiWatching
- */
- /**
- * @typedef {import("webpack").OutputFileSystem & { createReadStream?: import("fs").createReadStream, statSync: import("fs").statSync, readFileSync: import("fs").readFileSync }} OutputFileSystem
- */
- /** @typedef {ReturnType<Compiler["getInfrastructureLogger"]>} Logger */
- /**
- * @callback Callback
- * @param {(Stats | MultiStats)=} stats
- */
- /**
- * @typedef {object} ResponseData
- * @property {Buffer | ReadStream} data data
- * @property {number} byteLength byte length
- */
- /**
- * @template {IncomingMessage} [RequestInternal=IncomingMessage]
- * @template {ServerResponse} [ResponseInternal=ServerResponse]
- * @callback ModifyResponseData
- * @param {RequestInternal} req req
- * @param {ResponseInternal} res res
- * @param {Buffer | ReadStream} data data
- * @param {number} byteLength byte length
- * @returns {ResponseData}
- */
- /**
- * @template {IncomingMessage} [RequestInternal=IncomingMessage]
- * @template {ServerResponse} [ResponseInternal=ServerResponse]
- * @typedef {object} Context
- * @property {boolean} state state
- * @property {Stats | MultiStats | undefined} stats stats
- * @property {Callback[]} callbacks callbacks
- * @property {Options<RequestInternal, ResponseInternal>} options options
- * @property {Compiler | MultiCompiler} compiler compiler
- * @property {Watching | MultiWatching | undefined} watching watching
- * @property {Logger} logger logger
- * @property {OutputFileSystem} outputFileSystem output file system
- */
- /**
- * @template {IncomingMessage} [RequestInternal=IncomingMessage]
- * @template {ServerResponse} [ResponseInternal=ServerResponse]
- * @typedef {WithoutUndefined<Context<RequestInternal, ResponseInternal>, "watching">} FilledContext
- */
- /** @typedef {Record<string, string | number> | Array<{ key: string, value: number | string }>} NormalizedHeaders */
- /**
- * @template {IncomingMessage} [RequestInternal=IncomingMessage]
- * @template {ServerResponse} [ResponseInternal=ServerResponse]
- * @typedef {NormalizedHeaders | ((req: RequestInternal, res: ResponseInternal, context: Context<RequestInternal, ResponseInternal>) => void | undefined | NormalizedHeaders) | undefined} Headers
- */
- /**
- * @template {IncomingMessage} [RequestInternal = IncomingMessage]
- * @template {ServerResponse} [ResponseInternal = ServerResponse]
- * @typedef {object} Options
- * @property {{ [key: string]: string }=} mimeTypes mime types
- * @property {(string | undefined)=} mimeTypeDefault mime type default
- * @property {(boolean | ((targetPath: string) => boolean))=} writeToDisk write to disk
- * @property {string[]=} methods methods
- * @property {Headers<RequestInternal, ResponseInternal>=} headers headers
- * @property {NonNullable<Configuration["output"]>["publicPath"]=} publicPath public path
- * @property {Configuration["stats"]=} stats stats
- * @property {boolean=} serverSideRender is server side render
- * @property {OutputFileSystem=} outputFileSystem output file system
- * @property {(boolean | string)=} index index
- * @property {ModifyResponseData<RequestInternal, ResponseInternal>=} modifyResponseData modify response data
- * @property {"weak" | "strong"=} etag options to generate etag header
- * @property {boolean=} lastModified options to generate last modified header
- * @property {(boolean | number | string | { maxAge?: number, immutable?: boolean })=} cacheControl options to generate cache headers
- * @property {boolean=} cacheImmutable is cache immutable
- */
- /**
- * @template {IncomingMessage} [RequestInternal=IncomingMessage]
- * @template {ServerResponse} [ResponseInternal=ServerResponse]
- * @callback Middleware
- * @param {RequestInternal} req
- * @param {ResponseInternal} res
- * @param {NextFunction} next
- * @returns {Promise<void>}
- */
- /** @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 get filename from url
- * @property {WaitUntilValid} waitUntilValid wait until valid
- * @property {Invalidate} invalidate invalidate
- * @property {Close} close close
- * @property {Context<RequestInternal, ResponseInternal>} context context
- */
- /**
- * @template {IncomingMessage} [RequestInternal=IncomingMessage]
- * @template {ServerResponse} [ResponseInternal=ServerResponse]
- * @typedef {Middleware<RequestInternal, ResponseInternal> & AdditionalMethods<RequestInternal, ResponseInternal>} API
- */
- /**
- * @template T
- * @template {keyof T} K
- * @typedef {Omit<T, K> & Partial<T>} WithOptional
- */
- /**
- * @template T
- * @template {keyof T} K
- * @typedef {T & { [P in K]: NonNullable<T[P]> }} WithoutUndefined
- */
- /**
- * @template {IncomingMessage} [RequestInternal=IncomingMessage]
- * @template {ServerResponse} [ResponseInternal=ServerResponse]
- * @param {Compiler | MultiCompiler} compiler compiler
- * @param {Options<RequestInternal, ResponseInternal>=} options options
- * @returns {API<RequestInternal, ResponseInternal>} webpack dev middleware
- */
- 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-expect-error
- mime.types = {
- ...types,
- ...mimeTypes
- };
- }
- /**
- * @type {WithOptional<Context<RequestInternal, ResponseInternal>, "watching" | "outputFileSystem">}
- */
- const context = {
- state: false,
- 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 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 compilers = /** @type {MultiCompiler} */context.compiler;
- const watchOptions = compilers.compilers.map(childCompiler => childCompiler.options.watchOptions || {});
- context.watching = compiler.watch(watchOptions, errorHandler);
- } else {
- const oneCompiler = /** @type {Compiler} */context.compiler;
- const watchOptions = oneCompiler.options.watchOptions || {};
- context.watching = compiler.watch(watchOptions, errorHandler);
- }
- }
- const filledContext = /** @type {FilledContext<RequestInternal, ResponseInternal>} */
- context;
- const instance = /** @type {API<RequestInternal, ResponseInternal>} */
- 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;
- }
- /**
- * @template S
- * @template O
- * @typedef {object} HapiPluginBase
- * @property {(server: S, options: O) => void | Promise<void>} register register
- */
- /**
- * @template S
- * @template O
- * @typedef {HapiPluginBase<S, O> & { pkg: { name: string }, multiple: boolean }} HapiPlugin
- */
- /**
- * @typedef {Options & { compiler: Compiler | MultiCompiler }} HapiOptions
- */
- /**
- * @template HapiServer
- * @template {HapiOptions} HapiOptionsInternal
- * @returns {HapiPlugin<HapiServer, HapiOptionsInternal>} hapi wrapper
- */
- function hapiWrapper() {
- return {
- pkg: {
- name: "webpack-dev-middleware"
- },
- // Allow to have multiple middleware
- multiple: true,
- register(server, options) {
- const {
- compiler,
- ...rest
- } = options;
- if (!compiler) {
- throw new Error("The compiler options is required.");
- }
- const devMiddleware = wdm(compiler, rest);
- // @ts-expect-error
- if (!server.decorations.server.includes("webpackDevMiddleware")) {
- // @ts-expect-error
- server.decorate("server", "webpackDevMiddleware", devMiddleware);
- }
- // @ts-expect-error
- // eslint-disable-next-line id-length
- server.ext("onRequest", (request, h) => new Promise((resolve, reject) => {
- let isFinished = false;
- /**
- * @param {(string | Buffer)=} data
- */
- request.raw.res.send = data => {
- isFinished = true;
- request.raw.res.end(data);
- };
- /**
- * @param {(string | Buffer)=} data
- */
- request.raw.res.finish = data => {
- isFinished = true;
- request.raw.res.end(data);
- };
- devMiddleware(request.raw.req, request.raw.res, error => {
- if (error) {
- reject(error);
- return;
- }
- if (!isFinished) {
- resolve(request);
- }
- });
- }).then(() => h.continue).catch(error => {
- throw error;
- }));
- }
- };
- }
- wdm.hapiWrapper = hapiWrapper;
- // eslint-disable-next-line jsdoc/no-restricted-syntax
- /**
- * @template {IncomingMessage} [RequestInternal=IncomingMessage]
- * @template {ServerResponse} [ResponseInternal=ServerResponse]
- * @param {Compiler | MultiCompiler} compiler compiler
- * @param {Options<RequestInternal, ResponseInternal>=} options options
- * @returns {(ctx: any, next: Function) => Promise<void> | void} kow wrapper
- */
- function koaWrapper(compiler, options) {
- const devMiddleware = wdm(compiler, options);
- // eslint-disable-next-line jsdoc/no-restricted-syntax
- /**
- * @param {{req: RequestInternal, res: ResponseInternal & import("./utils/compatibleAPI").ExpectedServerResponse, status: number, body: string | Buffer | import("fs").ReadStream | {message: string}, state: object}} ctx context
- * @param {Function} next next
- * @returns {Promise<void>}
- */
- async function webpackDevMiddleware(ctx, next) {
- const {
- req,
- res
- } = ctx;
- res.locals = ctx.state;
- let {
- status
- } = ctx;
- /**
- * @returns {number} code
- */
- res.getStatusCode = () => status;
- /**
- * @param {number} statusCode status code
- */
- res.setStatusCode = statusCode => {
- status = statusCode;
- ctx.status = statusCode;
- };
- let isFinished = false;
- let needNext = false;
- try {
- await new Promise(
- /**
- * @param {(value: void) => void} resolve resolve
- * @param {(reason?: Error) => void} reject reject
- */
- (resolve, reject) => {
- /**
- * @param {import("fs").ReadStream} stream readable stream
- */
- res.stream = stream => {
- ctx.body = stream;
- isFinished = true;
- resolve();
- };
- /**
- * @param {string | Buffer} data data
- */
- res.send = data => {
- ctx.body = data;
- isFinished = true;
- resolve();
- };
- /**
- * @param {(string | Buffer)=} data data
- */
- res.finish = data => {
- ctx.status = status;
- res.end(data);
- isFinished = true;
- resolve();
- };
- devMiddleware(req, res, err => {
- if (err) {
- reject(err);
- return;
- }
- needNext = true;
- if (!isFinished) {
- resolve();
- }
- });
- });
- } catch (err) {
- ctx.status = /** @type {Error & { statusCode: number }} */err.statusCode || /** @type {Error & { status: number }} */err.status || 500;
- ctx.body = {
- message: /** @type {Error} */err.message
- };
- }
- if (needNext) {
- await next();
- }
- }
- webpackDevMiddleware.devMiddleware = devMiddleware;
- return webpackDevMiddleware;
- }
- wdm.koaWrapper = koaWrapper;
- // eslint-disable-next-line jsdoc/no-restricted-syntax
- /**
- * @template {IncomingMessage} [RequestInternal=IncomingMessage]
- * @template {ServerResponse} [ResponseInternal=ServerResponse]
- * @param {Compiler | MultiCompiler} compiler compiler
- * @param {Options<RequestInternal, ResponseInternal>=} options options
- * @returns {(ctx: any, next: Function) => Promise<void> | void} hono wrapper
- */
- function honoWrapper(compiler, options) {
- const devMiddleware = wdm(compiler, options);
- // eslint-disable-next-line jsdoc/no-restricted-syntax
- /**
- * @param {{ env: any, body: any, json: any, status: any, set: any, req: RequestInternal & import("./utils/compatibleAPI").ExpectedIncomingMessage & { header: (name: string) => string }, res: ResponseInternal & import("./utils/compatibleAPI").ExpectedServerResponse & { headers: any, status: any } }} context context
- * @param {Function} next next function
- * @returns {Promise<void>}
- */
- async function webpackDevMiddleware(context, next) {
- const {
- req,
- res
- } = context;
- context.set("webpack", {
- devMiddleware: devMiddleware.context
- });
- /**
- * @returns {string | undefined} method
- */
- req.getMethod = () => context.req.method;
- /**
- * @param {string} name name
- * @returns {string | string[] | undefined} header value
- */
- req.getHeader = name => context.req.header(name);
- /**
- * @returns {string | undefined} URL
- */
- req.getURL = () => context.req.url;
- let {
- status
- } = context.res;
- /**
- * @returns {number} code code
- */
- res.getStatusCode = () => status;
- /**
- * @param {number} code code
- */
- res.setStatusCode = code => {
- status = code;
- };
- /**
- * @param {string} name header name
- * @returns {string | string[] | undefined} header
- */
- res.getHeader = name => context.res.headers.get(name);
- // eslint-disable-next-line jsdoc/no-restricted-syntax
- /**
- * @param {string} name header name
- * @param {string | number | Readonly<string[]>} value value
- * @returns {ResponseInternal & import("./utils/compatibleAPI").ExpectedServerResponse & { headers: any, status: any }} response
- */
- res.setHeader = (name, value) => {
- context.res.headers.append(name, value);
- return context.res;
- };
- /**
- * @param {string} name header name
- */
- res.removeHeader = name => {
- context.res.headers.delete(name);
- };
- /**
- * @returns {string[]} response headers
- */
- res.getResponseHeaders = () => [...context.res.headers.keys()];
- /**
- * @returns {ServerResponse} server response
- */
- res.getOutgoing = () => context.env.outgoing;
- res.setState = () => {
- // Do nothing, because we set it before
- };
- res.getHeadersSent = () => context.env.outgoing.headersSent;
- let body;
- let isFinished = false;
- try {
- await new Promise(
- /**
- * @param {(value: void) => void} resolve resolve
- * @param {(reason?: Error) => void} reject reject
- */
- (resolve, reject) => {
- /**
- * @param {import("fs").ReadStream} stream readable stream
- */
- res.stream = stream => {
- body = stream;
- isFinished = true;
- resolve();
- };
- /**
- * @param {string | Buffer} data data
- */
- res.send = data => {
- // Hono sets `Content-Length` by default
- context.res.headers.delete("Content-Length");
- body = data;
- isFinished = true;
- resolve();
- };
- /**
- * @param {(string | Buffer)=} data data
- */
- res.finish = data => {
- const isDataExist = typeof data !== "undefined";
- // Hono sets `Content-Length` by default
- if (isDataExist) {
- context.res.headers.delete("Content-Length");
- }
- body = isDataExist ? data : null;
- isFinished = true;
- resolve();
- };
- devMiddleware(req, res, err => {
- if (err) {
- reject(err);
- return;
- }
- if (!isFinished) {
- resolve();
- }
- });
- });
- } catch (err) {
- context.status(500);
- return context.json({
- message: /** @type {Error} */err.message
- });
- }
- if (typeof body !== "undefined") {
- return context.body(body, status);
- }
- await next();
- }
- webpackDevMiddleware.devMiddleware = devMiddleware;
- return webpackDevMiddleware;
- }
- wdm.honoWrapper = honoWrapper;
- module.exports = wdm;
|