index.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. "use strict";
  2. const {
  3. validate
  4. } = require("schema-utils");
  5. const mime = require("mime-types");
  6. const middleware = require("./middleware");
  7. const getFilenameFromUrl = require("./utils/getFilenameFromUrl");
  8. const setupHooks = require("./utils/setupHooks");
  9. const setupWriteToDisk = require("./utils/setupWriteToDisk");
  10. const setupOutputFileSystem = require("./utils/setupOutputFileSystem");
  11. const ready = require("./utils/ready");
  12. const schema = require("./options.json");
  13. const noop = () => {};
  14. /** @typedef {import("schema-utils/declarations/validate").Schema} Schema */
  15. /** @typedef {import("webpack").Compiler} Compiler */
  16. /** @typedef {import("webpack").MultiCompiler} MultiCompiler */
  17. /** @typedef {import("webpack").Configuration} Configuration */
  18. /** @typedef {import("webpack").Stats} Stats */
  19. /** @typedef {import("webpack").MultiStats} MultiStats */
  20. /** @typedef {import("fs").ReadStream} ReadStream */
  21. /**
  22. * @typedef {Object} ExtendedServerResponse
  23. * @property {{ webpack?: { devMiddleware?: Context<IncomingMessage, ServerResponse> } }} [locals]
  24. */
  25. /** @typedef {import("http").IncomingMessage} IncomingMessage */
  26. /** @typedef {import("http").ServerResponse & ExtendedServerResponse} ServerResponse */
  27. /**
  28. * @callback NextFunction
  29. * @param {any} [err]
  30. * @return {void}
  31. */
  32. /**
  33. * @typedef {NonNullable<Configuration["watchOptions"]>} WatchOptions
  34. */
  35. /**
  36. * @typedef {Compiler["watching"]} Watching
  37. */
  38. /**
  39. * @typedef {ReturnType<Compiler["watch"]>} MultiWatching
  40. */
  41. /**
  42. * @typedef {Compiler["outputFileSystem"] & { createReadStream?: import("fs").createReadStream, statSync?: import("fs").statSync, lstat?: import("fs").lstat, readFileSync?: import("fs").readFileSync }} OutputFileSystem
  43. */
  44. /** @typedef {ReturnType<Compiler["getInfrastructureLogger"]>} Logger */
  45. /**
  46. * @callback Callback
  47. * @param {Stats | MultiStats} [stats]
  48. */
  49. /**
  50. * @typedef {Object} ResponseData
  51. * @property {string | Buffer | ReadStream} data
  52. * @property {number} byteLength
  53. */
  54. /**
  55. * @template {IncomingMessage} RequestInternal
  56. * @template {ServerResponse} ResponseInternal
  57. * @callback ModifyResponseData
  58. * @param {RequestInternal} req
  59. * @param {ResponseInternal} res
  60. * @param {string | Buffer | ReadStream} data
  61. * @param {number} byteLength
  62. * @return {ResponseData}
  63. */
  64. /**
  65. * @template {IncomingMessage} RequestInternal
  66. * @template {ServerResponse} ResponseInternal
  67. * @typedef {Object} Context
  68. * @property {boolean} state
  69. * @property {Stats | MultiStats | undefined} stats
  70. * @property {Callback[]} callbacks
  71. * @property {Options<RequestInternal, ResponseInternal>} options
  72. * @property {Compiler | MultiCompiler} compiler
  73. * @property {Watching | MultiWatching} watching
  74. * @property {Logger} logger
  75. * @property {OutputFileSystem} outputFileSystem
  76. */
  77. /**
  78. * @template {IncomingMessage} RequestInternal
  79. * @template {ServerResponse} ResponseInternal
  80. * @typedef {Record<string, string | number> | Array<{ key: string, value: number | string }> | ((req: RequestInternal, res: ResponseInternal, context: Context<RequestInternal, ResponseInternal>) => void | undefined | Record<string, string | number>) | undefined} Headers
  81. */
  82. /**
  83. * @template {IncomingMessage} RequestInternal
  84. * @template {ServerResponse} ResponseInternal
  85. * @typedef {Object} Options
  86. * @property {{[key: string]: string}} [mimeTypes]
  87. * @property {string | undefined} [mimeTypeDefault]
  88. * @property {boolean | ((targetPath: string) => boolean)} [writeToDisk]
  89. * @property {string[]} [methods]
  90. * @property {Headers<RequestInternal, ResponseInternal>} [headers]
  91. * @property {NonNullable<Configuration["output"]>["publicPath"]} [publicPath]
  92. * @property {Configuration["stats"]} [stats]
  93. * @property {boolean} [serverSideRender]
  94. * @property {OutputFileSystem} [outputFileSystem]
  95. * @property {boolean | string} [index]
  96. * @property {ModifyResponseData<RequestInternal, ResponseInternal>} [modifyResponseData]
  97. */
  98. /**
  99. * @template {IncomingMessage} RequestInternal
  100. * @template {ServerResponse} ResponseInternal
  101. * @callback Middleware
  102. * @param {RequestInternal} req
  103. * @param {ResponseInternal} res
  104. * @param {NextFunction} next
  105. * @return {Promise<void>}
  106. */
  107. /**
  108. * @callback GetFilenameFromUrl
  109. * @param {string} url
  110. * @returns {string | undefined}
  111. */
  112. /**
  113. * @callback WaitUntilValid
  114. * @param {Callback} callback
  115. */
  116. /**
  117. * @callback Invalidate
  118. * @param {Callback} callback
  119. */
  120. /**
  121. * @callback Close
  122. * @param {(err: Error | null | undefined) => void} callback
  123. */
  124. /**
  125. * @template {IncomingMessage} RequestInternal
  126. * @template {ServerResponse} ResponseInternal
  127. * @typedef {Object} AdditionalMethods
  128. * @property {GetFilenameFromUrl} getFilenameFromUrl
  129. * @property {WaitUntilValid} waitUntilValid
  130. * @property {Invalidate} invalidate
  131. * @property {Close} close
  132. * @property {Context<RequestInternal, ResponseInternal>} context
  133. */
  134. /**
  135. * @template {IncomingMessage} RequestInternal
  136. * @template {ServerResponse} ResponseInternal
  137. * @typedef {Middleware<RequestInternal, ResponseInternal> & AdditionalMethods<RequestInternal, ResponseInternal>} API
  138. */
  139. /**
  140. * @template {IncomingMessage} RequestInternal
  141. * @template {ServerResponse} ResponseInternal
  142. * @param {Compiler | MultiCompiler} compiler
  143. * @param {Options<RequestInternal, ResponseInternal>} [options]
  144. * @returns {API<RequestInternal, ResponseInternal>}
  145. */
  146. function wdm(compiler, options = {}) {
  147. validate( /** @type {Schema} */schema, options, {
  148. name: "Dev Middleware",
  149. baseDataPath: "options"
  150. });
  151. const {
  152. mimeTypes
  153. } = options;
  154. if (mimeTypes) {
  155. const {
  156. types
  157. } = mime;
  158. // mimeTypes from user provided options should take priority
  159. // over existing, known types
  160. // @ts-ignore
  161. mime.types = {
  162. ...types,
  163. ...mimeTypes
  164. };
  165. }
  166. /**
  167. * @type {Context<RequestInternal, ResponseInternal>}
  168. */
  169. const context = {
  170. state: false,
  171. // eslint-disable-next-line no-undefined
  172. stats: undefined,
  173. callbacks: [],
  174. options,
  175. compiler,
  176. // @ts-ignore
  177. // eslint-disable-next-line no-undefined
  178. watching: undefined,
  179. logger: compiler.getInfrastructureLogger("webpack-dev-middleware"),
  180. // @ts-ignore
  181. // eslint-disable-next-line no-undefined
  182. outputFileSystem: undefined
  183. };
  184. setupHooks(context);
  185. if (options.writeToDisk) {
  186. setupWriteToDisk(context);
  187. }
  188. setupOutputFileSystem(context);
  189. // Start watching
  190. if ( /** @type {Compiler} */context.compiler.watching) {
  191. context.watching = /** @type {Compiler} */context.compiler.watching;
  192. } else {
  193. /**
  194. * @type {WatchOptions | WatchOptions[]}
  195. */
  196. let watchOptions;
  197. /**
  198. * @param {Error | null | undefined} error
  199. */
  200. const errorHandler = error => {
  201. if (error) {
  202. // TODO: improve that in future
  203. // For example - `writeToDisk` can throw an error and right now it is ends watching.
  204. // We can improve that and keep watching active, but it is require API on webpack side.
  205. // Let's implement that in webpack@5 because it is rare case.
  206. context.logger.error(error);
  207. }
  208. };
  209. if (Array.isArray( /** @type {MultiCompiler} */context.compiler.compilers)) {
  210. watchOptions = /** @type {MultiCompiler} */
  211. context.compiler.compilers.map(
  212. /**
  213. * @param {Compiler} childCompiler
  214. * @returns {WatchOptions}
  215. */
  216. childCompiler => childCompiler.options.watchOptions || {});
  217. context.watching = /** @type {MultiWatching} */
  218. context.compiler.watch( /** @type {WatchOptions}} */
  219. watchOptions, errorHandler);
  220. } else {
  221. watchOptions = /** @type {Compiler} */context.compiler.options.watchOptions || {};
  222. context.watching = /** @type {Watching} */
  223. context.compiler.watch(watchOptions, errorHandler);
  224. }
  225. }
  226. const instance = /** @type {API<RequestInternal, ResponseInternal>} */
  227. middleware(context);
  228. // API
  229. /** @type {API<RequestInternal, ResponseInternal>} */
  230. instance.getFilenameFromUrl =
  231. /**
  232. * @param {string} url
  233. * @returns {string|undefined}
  234. */
  235. url => getFilenameFromUrl(context, url);
  236. /** @type {API<RequestInternal, ResponseInternal>} */
  237. instance.waitUntilValid = (callback = noop) => {
  238. ready(context, callback);
  239. };
  240. /** @type {API<RequestInternal, ResponseInternal>} */
  241. instance.invalidate = (callback = noop) => {
  242. ready(context, callback);
  243. context.watching.invalidate();
  244. };
  245. /** @type {API<RequestInternal, ResponseInternal>} */
  246. instance.close = (callback = noop) => {
  247. context.watching.close(callback);
  248. };
  249. /** @type {API<RequestInternal, ResponseInternal>} */
  250. instance.context = context;
  251. return instance;
  252. }
  253. module.exports = wdm;