index.js 7.6 KB

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