Server.js 93 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238
  1. "use strict";
  2. const os = require("os");
  3. const path = require("path");
  4. const url = require("url");
  5. const util = require("util");
  6. const fs = require("graceful-fs");
  7. const ipaddr = require("ipaddr.js");
  8. const { validate } = require("schema-utils");
  9. const schema = require("./options.json");
  10. /** @typedef {import("schema-utils/declarations/validate").Schema} Schema */
  11. /** @typedef {import("webpack").Compiler} Compiler */
  12. /** @typedef {import("webpack").MultiCompiler} MultiCompiler */
  13. /** @typedef {import("webpack").Configuration} WebpackConfiguration */
  14. /** @typedef {import("webpack").StatsOptions} StatsOptions */
  15. /** @typedef {import("webpack").StatsCompilation} StatsCompilation */
  16. /** @typedef {import("webpack").Stats} Stats */
  17. /** @typedef {import("webpack").MultiStats} MultiStats */
  18. /** @typedef {import("os").NetworkInterfaceInfo} NetworkInterfaceInfo */
  19. /** @typedef {import("express").NextFunction} NextFunction */
  20. /** @typedef {import("express").RequestHandler} ExpressRequestHandler */
  21. /** @typedef {import("express").ErrorRequestHandler} ExpressErrorRequestHandler */
  22. /** @typedef {import("chokidar").WatchOptions} WatchOptions */
  23. /** @typedef {import("chokidar").FSWatcher} FSWatcher */
  24. /** @typedef {import("connect-history-api-fallback").Options} ConnectHistoryApiFallbackOptions */
  25. /** @typedef {import("bonjour-service").Bonjour} Bonjour */
  26. /** @typedef {import("bonjour-service").Service} BonjourOptions */
  27. /** @typedef {import("http-proxy-middleware").RequestHandler} RequestHandler */
  28. /** @typedef {import("http-proxy-middleware").Options} HttpProxyMiddlewareOptions */
  29. /** @typedef {import("http-proxy-middleware").Filter} HttpProxyMiddlewareOptionsFilter */
  30. /** @typedef {import("serve-index").Options} ServeIndexOptions */
  31. /** @typedef {import("serve-static").ServeStaticOptions} ServeStaticOptions */
  32. /** @typedef {import("ipaddr.js").IPv4} IPv4 */
  33. /** @typedef {import("ipaddr.js").IPv6} IPv6 */
  34. /** @typedef {import("net").Socket} Socket */
  35. /** @typedef {import("http").IncomingMessage} IncomingMessage */
  36. /** @typedef {import("http").ServerResponse} ServerResponse */
  37. /** @typedef {import("open").Options} OpenOptions */
  38. /** @typedef {import("https").ServerOptions & { spdy?: { plain?: boolean | undefined, ssl?: boolean | undefined, 'x-forwarded-for'?: string | undefined, protocol?: string | undefined, protocols?: string[] | undefined }}} ServerOptions */
  39. /** @typedef {import("express").Request} Request */
  40. /** @typedef {import("express").Response} Response */
  41. /**
  42. * @template {Request} T
  43. * @template {Response} U
  44. * @typedef {import("webpack-dev-middleware").Options<T, U>} DevMiddlewareOptions
  45. */
  46. /**
  47. * @template {Request} T
  48. * @template {Response} U
  49. * @typedef {import("webpack-dev-middleware").Context<T, U>} DevMiddlewareContext
  50. */
  51. /**
  52. * @typedef {"local-ip" | "local-ipv4" | "local-ipv6" | string} Host
  53. */
  54. /**
  55. * @typedef {number | string | "auto"} Port
  56. */
  57. /**
  58. * @typedef {Object} WatchFiles
  59. * @property {string | string[]} paths
  60. * @property {WatchOptions & { aggregateTimeout?: number, ignored?: WatchOptions["ignored"], poll?: number | boolean }} [options]
  61. */
  62. /**
  63. * @typedef {Object} Static
  64. * @property {string} [directory]
  65. * @property {string | string[]} [publicPath]
  66. * @property {boolean | ServeIndexOptions} [serveIndex]
  67. * @property {ServeStaticOptions} [staticOptions]
  68. * @property {boolean | WatchOptions & { aggregateTimeout?: number, ignored?: WatchOptions["ignored"], poll?: number | boolean }} [watch]
  69. */
  70. /**
  71. * @typedef {Object} NormalizedStatic
  72. * @property {string} directory
  73. * @property {string[]} publicPath
  74. * @property {false | ServeIndexOptions} serveIndex
  75. * @property {ServeStaticOptions} staticOptions
  76. * @property {false | WatchOptions} watch
  77. */
  78. /**
  79. * @typedef {Object} ServerConfiguration
  80. * @property {"http" | "https" | "spdy" | string} [type]
  81. * @property {ServerOptions} [options]
  82. */
  83. /**
  84. * @typedef {Object} WebSocketServerConfiguration
  85. * @property {"sockjs" | "ws" | string | Function} [type]
  86. * @property {Record<string, any>} [options]
  87. */
  88. /**
  89. * @typedef {(import("ws").WebSocket | import("sockjs").Connection & { send: import("ws").WebSocket["send"], terminate: import("ws").WebSocket["terminate"], ping: import("ws").WebSocket["ping"] }) & { isAlive?: boolean }} ClientConnection
  90. */
  91. /**
  92. * @typedef {import("ws").WebSocketServer | import("sockjs").Server & { close: import("ws").WebSocketServer["close"] }} WebSocketServer
  93. */
  94. /**
  95. * @typedef {{ implementation: WebSocketServer, clients: ClientConnection[] }} WebSocketServerImplementation
  96. */
  97. /**
  98. * @callback ByPass
  99. * @param {Request} req
  100. * @param {Response} res
  101. * @param {ProxyConfigArrayItem} proxyConfig
  102. */
  103. /**
  104. * @typedef {{ path?: HttpProxyMiddlewareOptionsFilter | undefined, context?: HttpProxyMiddlewareOptionsFilter | undefined } & { bypass?: ByPass } & HttpProxyMiddlewareOptions } ProxyConfigArrayItem
  105. */
  106. /**
  107. * @typedef {(ProxyConfigArrayItem | ((req?: Request | undefined, res?: Response | undefined, next?: NextFunction | undefined) => ProxyConfigArrayItem))[]} ProxyConfigArray
  108. */
  109. /**
  110. * @typedef {Object} OpenApp
  111. * @property {string} [name]
  112. * @property {string[]} [arguments]
  113. */
  114. /**
  115. * @typedef {Object} Open
  116. * @property {string | string[] | OpenApp} [app]
  117. * @property {string | string[]} [target]
  118. */
  119. /**
  120. * @typedef {Object} NormalizedOpen
  121. * @property {string} target
  122. * @property {import("open").Options} options
  123. */
  124. /**
  125. * @typedef {Object} WebSocketURL
  126. * @property {string} [hostname]
  127. * @property {string} [password]
  128. * @property {string} [pathname]
  129. * @property {number | string} [port]
  130. * @property {string} [protocol]
  131. * @property {string} [username]
  132. */
  133. /**
  134. * @typedef {boolean | ((error: Error) => void)} OverlayMessageOptions
  135. */
  136. /**
  137. * @typedef {Object} ClientConfiguration
  138. * @property {"log" | "info" | "warn" | "error" | "none" | "verbose"} [logging]
  139. * @property {boolean | { warnings?: OverlayMessageOptions, errors?: OverlayMessageOptions, runtimeErrors?: OverlayMessageOptions }} [overlay]
  140. * @property {boolean} [progress]
  141. * @property {boolean | number} [reconnect]
  142. * @property {"ws" | "sockjs" | string} [webSocketTransport]
  143. * @property {string | WebSocketURL} [webSocketURL]
  144. */
  145. /**
  146. * @typedef {Array<{ key: string; value: string }> | Record<string, string | string[]>} Headers
  147. */
  148. /**
  149. * @typedef {{ name?: string, path?: string, middleware: ExpressRequestHandler | ExpressErrorRequestHandler } | ExpressRequestHandler | ExpressErrorRequestHandler} Middleware
  150. */
  151. /**
  152. * @typedef {Object} Configuration
  153. * @property {boolean | string} [ipc]
  154. * @property {Host} [host]
  155. * @property {Port} [port]
  156. * @property {boolean | "only"} [hot]
  157. * @property {boolean} [liveReload]
  158. * @property {DevMiddlewareOptions<Request, Response>} [devMiddleware]
  159. * @property {boolean} [compress]
  160. * @property {"auto" | "all" | string | string[]} [allowedHosts]
  161. * @property {boolean | ConnectHistoryApiFallbackOptions} [historyApiFallback]
  162. * @property {boolean | Record<string, never> | BonjourOptions} [bonjour]
  163. * @property {string | string[] | WatchFiles | Array<string | WatchFiles>} [watchFiles]
  164. * @property {boolean | string | Static | Array<string | Static>} [static]
  165. * @property {boolean | ServerOptions} [https]
  166. * @property {boolean} [http2]
  167. * @property {"http" | "https" | "spdy" | string | ServerConfiguration} [server]
  168. * @property {boolean | "sockjs" | "ws" | string | WebSocketServerConfiguration} [webSocketServer]
  169. * @property {ProxyConfigArray} [proxy]
  170. * @property {boolean | string | Open | Array<string | Open>} [open]
  171. * @property {boolean} [setupExitSignals]
  172. * @property {boolean | ClientConfiguration} [client]
  173. * @property {Headers | ((req: Request, res: Response, context: DevMiddlewareContext<Request, Response>) => Headers)} [headers]
  174. * @property {(devServer: Server) => void} [onListening]
  175. * @property {(middlewares: Middleware[], devServer: Server) => Middleware[]} [setupMiddlewares]
  176. */
  177. if (!process.env.WEBPACK_SERVE) {
  178. process.env.WEBPACK_SERVE = "true";
  179. }
  180. /**
  181. * @template T
  182. * @param fn {(function(): any) | undefined}
  183. * @returns {function(): T}
  184. */
  185. const memoize = (fn) => {
  186. let cache = false;
  187. /** @type {T} */
  188. let result;
  189. return () => {
  190. if (cache) {
  191. return result;
  192. }
  193. result = /** @type {function(): any} */ (fn)();
  194. cache = true;
  195. // Allow to clean up memory for fn
  196. // and all dependent resources
  197. // eslint-disable-next-line no-undefined
  198. fn = undefined;
  199. return result;
  200. };
  201. };
  202. const getExpress = memoize(() => require("express"));
  203. /**
  204. *
  205. * @param {OverlayMessageOptions} [setting]
  206. * @returns
  207. */
  208. const encodeOverlaySettings = (setting) =>
  209. typeof setting === "function"
  210. ? encodeURIComponent(setting.toString())
  211. : setting;
  212. class Server {
  213. /**
  214. * @param {Configuration | Compiler | MultiCompiler} options
  215. * @param {Compiler | MultiCompiler | Configuration} compiler
  216. */
  217. constructor(options = {}, compiler) {
  218. validate(/** @type {Schema} */ (schema), options, {
  219. name: "Dev Server",
  220. baseDataPath: "options",
  221. });
  222. this.compiler = /** @type {Compiler | MultiCompiler} */ (compiler);
  223. /**
  224. * @type {ReturnType<Compiler["getInfrastructureLogger"]>}
  225. * */
  226. this.logger = this.compiler.getInfrastructureLogger("webpack-dev-server");
  227. this.options = /** @type {Configuration} */ (options);
  228. /**
  229. * @type {FSWatcher[]}
  230. */
  231. this.staticWatchers = [];
  232. /**
  233. * @private
  234. * @type {{ name: string | symbol, listener: (...args: any[]) => void}[] }}
  235. */
  236. this.listeners = [];
  237. // Keep track of websocket proxies for external websocket upgrade.
  238. /**
  239. * @private
  240. * @type {RequestHandler[]}
  241. */
  242. this.webSocketProxies = [];
  243. /**
  244. * @type {Socket[]}
  245. */
  246. this.sockets = [];
  247. /**
  248. * @private
  249. * @type {string | undefined}
  250. */
  251. // eslint-disable-next-line no-undefined
  252. this.currentHash = undefined;
  253. }
  254. static get schema() {
  255. return schema;
  256. }
  257. /**
  258. * @private
  259. * @returns {StatsOptions}
  260. * @constructor
  261. */
  262. static get DEFAULT_STATS() {
  263. return {
  264. all: false,
  265. hash: true,
  266. warnings: true,
  267. errors: true,
  268. errorDetails: false,
  269. };
  270. }
  271. /**
  272. * @param {string} URL
  273. * @returns {boolean}
  274. */
  275. static isAbsoluteURL(URL) {
  276. // Don't match Windows paths `c:\`
  277. if (/^[a-zA-Z]:\\/.test(URL)) {
  278. return false;
  279. }
  280. // Scheme: https://tools.ietf.org/html/rfc3986#section-3.1
  281. // Absolute URL: https://tools.ietf.org/html/rfc3986#section-4.3
  282. return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(URL);
  283. }
  284. /**
  285. * @param {string} gateway
  286. * @returns {string | undefined}
  287. */
  288. static findIp(gateway) {
  289. const gatewayIp = ipaddr.parse(gateway);
  290. // Look for the matching interface in all local interfaces.
  291. for (const addresses of Object.values(os.networkInterfaces())) {
  292. for (const { cidr } of /** @type {NetworkInterfaceInfo[]} */ (
  293. addresses
  294. )) {
  295. const net = ipaddr.parseCIDR(/** @type {string} */ (cidr));
  296. if (
  297. net[0] &&
  298. net[0].kind() === gatewayIp.kind() &&
  299. gatewayIp.match(net)
  300. ) {
  301. return net[0].toString();
  302. }
  303. }
  304. }
  305. }
  306. /**
  307. * @param {"v4" | "v6"} family
  308. * @returns {Promise<string | undefined>}
  309. */
  310. static async internalIP(family) {
  311. try {
  312. const { gateway } = await require("default-gateway")[family]();
  313. return Server.findIp(gateway);
  314. } catch {
  315. // ignore
  316. }
  317. }
  318. /**
  319. * @param {"v4" | "v6"} family
  320. * @returns {string | undefined}
  321. */
  322. static internalIPSync(family) {
  323. try {
  324. const { gateway } = require("default-gateway")[family].sync();
  325. return Server.findIp(gateway);
  326. } catch {
  327. // ignore
  328. }
  329. }
  330. /**
  331. * @param {Host} hostname
  332. * @returns {Promise<string>}
  333. */
  334. static async getHostname(hostname) {
  335. if (hostname === "local-ip") {
  336. return (
  337. (await Server.internalIP("v4")) ||
  338. (await Server.internalIP("v6")) ||
  339. "0.0.0.0"
  340. );
  341. } else if (hostname === "local-ipv4") {
  342. return (await Server.internalIP("v4")) || "0.0.0.0";
  343. } else if (hostname === "local-ipv6") {
  344. return (await Server.internalIP("v6")) || "::";
  345. }
  346. return hostname;
  347. }
  348. /**
  349. * @param {Port} port
  350. * @param {string} host
  351. * @returns {Promise<number | string>}
  352. */
  353. static async getFreePort(port, host) {
  354. if (typeof port !== "undefined" && port !== null && port !== "auto") {
  355. return port;
  356. }
  357. const pRetry = (await import("p-retry")).default;
  358. const getPort = require("./getPort");
  359. const basePort =
  360. typeof process.env.WEBPACK_DEV_SERVER_BASE_PORT !== "undefined"
  361. ? parseInt(process.env.WEBPACK_DEV_SERVER_BASE_PORT, 10)
  362. : 8080;
  363. // Try to find unused port and listen on it for 3 times,
  364. // if port is not specified in options.
  365. const defaultPortRetry =
  366. typeof process.env.WEBPACK_DEV_SERVER_PORT_RETRY !== "undefined"
  367. ? parseInt(process.env.WEBPACK_DEV_SERVER_PORT_RETRY, 10)
  368. : 3;
  369. return pRetry(() => getPort(basePort, host), {
  370. retries: defaultPortRetry,
  371. });
  372. }
  373. /**
  374. * @returns {string}
  375. */
  376. static findCacheDir() {
  377. const cwd = process.cwd();
  378. /**
  379. * @type {string | undefined}
  380. */
  381. let dir = cwd;
  382. for (;;) {
  383. try {
  384. if (fs.statSync(path.join(dir, "package.json")).isFile()) break;
  385. // eslint-disable-next-line no-empty
  386. } catch (e) {}
  387. const parent = path.dirname(dir);
  388. if (dir === parent) {
  389. // eslint-disable-next-line no-undefined
  390. dir = undefined;
  391. break;
  392. }
  393. dir = parent;
  394. }
  395. if (!dir) {
  396. return path.resolve(cwd, ".cache/webpack-dev-server");
  397. } else if (process.versions.pnp === "1") {
  398. return path.resolve(dir, ".pnp/.cache/webpack-dev-server");
  399. } else if (process.versions.pnp === "3") {
  400. return path.resolve(dir, ".yarn/.cache/webpack-dev-server");
  401. }
  402. return path.resolve(dir, "node_modules/.cache/webpack-dev-server");
  403. }
  404. /**
  405. * @private
  406. * @param {Compiler} compiler
  407. * @returns bool
  408. */
  409. static isWebTarget(compiler) {
  410. // TODO improve for the next major version - we should store `web` and other targets in `compiler.options.environment`
  411. if (
  412. compiler.options.externalsPresets &&
  413. compiler.options.externalsPresets.web
  414. ) {
  415. return true;
  416. }
  417. if (
  418. compiler.options.resolve.conditionNames &&
  419. compiler.options.resolve.conditionNames.includes("browser")
  420. ) {
  421. return true;
  422. }
  423. const webTargets = [
  424. "web",
  425. "webworker",
  426. "electron-preload",
  427. "electron-renderer",
  428. "node-webkit",
  429. // eslint-disable-next-line no-undefined
  430. undefined,
  431. null,
  432. ];
  433. if (Array.isArray(compiler.options.target)) {
  434. return compiler.options.target.some((r) => webTargets.includes(r));
  435. }
  436. return webTargets.includes(/** @type {string} */ (compiler.options.target));
  437. }
  438. /**
  439. * @private
  440. * @param {Compiler} compiler
  441. */
  442. addAdditionalEntries(compiler) {
  443. /**
  444. * @type {string[]}
  445. */
  446. const additionalEntries = [];
  447. const isWebTarget = Server.isWebTarget(compiler);
  448. // TODO maybe empty client
  449. if (this.options.client && isWebTarget) {
  450. let webSocketURLStr = "";
  451. if (this.options.webSocketServer) {
  452. const webSocketURL =
  453. /** @type {WebSocketURL} */
  454. (
  455. /** @type {ClientConfiguration} */
  456. (this.options.client).webSocketURL
  457. );
  458. const webSocketServer =
  459. /** @type {{ type: WebSocketServerConfiguration["type"], options: NonNullable<WebSocketServerConfiguration["options"]> }} */
  460. (this.options.webSocketServer);
  461. const searchParams = new URLSearchParams();
  462. /** @type {string} */
  463. let protocol;
  464. // We are proxying dev server and need to specify custom `hostname`
  465. if (typeof webSocketURL.protocol !== "undefined") {
  466. protocol = webSocketURL.protocol;
  467. } else {
  468. protocol =
  469. /** @type {ServerConfiguration} */
  470. (this.options.server).type === "http" ? "ws:" : "wss:";
  471. }
  472. searchParams.set("protocol", protocol);
  473. if (typeof webSocketURL.username !== "undefined") {
  474. searchParams.set("username", webSocketURL.username);
  475. }
  476. if (typeof webSocketURL.password !== "undefined") {
  477. searchParams.set("password", webSocketURL.password);
  478. }
  479. /** @type {string} */
  480. let hostname;
  481. // SockJS is not supported server mode, so `hostname` and `port` can't specified, let's ignore them
  482. const isSockJSType = webSocketServer.type === "sockjs";
  483. const isWebSocketServerHostDefined =
  484. typeof webSocketServer.options.host !== "undefined";
  485. const isWebSocketServerPortDefined =
  486. typeof webSocketServer.options.port !== "undefined";
  487. if (
  488. isSockJSType &&
  489. (isWebSocketServerHostDefined || isWebSocketServerPortDefined)
  490. ) {
  491. this.logger.warn(
  492. "SockJS only supports client mode and does not support custom hostname and port options. Please consider using 'ws' if you need to customize these options.",
  493. );
  494. }
  495. // We are proxying dev server and need to specify custom `hostname`
  496. if (typeof webSocketURL.hostname !== "undefined") {
  497. hostname = webSocketURL.hostname;
  498. }
  499. // Web socket server works on custom `hostname`, only for `ws` because `sock-js` is not support custom `hostname`
  500. else if (isWebSocketServerHostDefined && !isSockJSType) {
  501. hostname = webSocketServer.options.host;
  502. }
  503. // The `host` option is specified
  504. else if (typeof this.options.host !== "undefined") {
  505. hostname = this.options.host;
  506. }
  507. // The `port` option is not specified
  508. else {
  509. hostname = "0.0.0.0";
  510. }
  511. searchParams.set("hostname", hostname);
  512. /** @type {number | string} */
  513. let port;
  514. // We are proxying dev server and need to specify custom `port`
  515. if (typeof webSocketURL.port !== "undefined") {
  516. port = webSocketURL.port;
  517. }
  518. // Web socket server works on custom `port`, only for `ws` because `sock-js` is not support custom `port`
  519. else if (isWebSocketServerPortDefined && !isSockJSType) {
  520. port = webSocketServer.options.port;
  521. }
  522. // The `port` option is specified
  523. else if (typeof this.options.port === "number") {
  524. port = this.options.port;
  525. }
  526. // The `port` option is specified using `string`
  527. else if (
  528. typeof this.options.port === "string" &&
  529. this.options.port !== "auto"
  530. ) {
  531. port = Number(this.options.port);
  532. }
  533. // The `port` option is not specified or set to `auto`
  534. else {
  535. port = "0";
  536. }
  537. searchParams.set("port", String(port));
  538. /** @type {string} */
  539. let pathname = "";
  540. // We are proxying dev server and need to specify custom `pathname`
  541. if (typeof webSocketURL.pathname !== "undefined") {
  542. pathname = webSocketURL.pathname;
  543. }
  544. // Web socket server works on custom `path`
  545. else if (
  546. typeof webSocketServer.options.prefix !== "undefined" ||
  547. typeof webSocketServer.options.path !== "undefined"
  548. ) {
  549. pathname =
  550. webSocketServer.options.prefix || webSocketServer.options.path;
  551. }
  552. searchParams.set("pathname", pathname);
  553. const client = /** @type {ClientConfiguration} */ (this.options.client);
  554. if (typeof client.logging !== "undefined") {
  555. searchParams.set("logging", client.logging);
  556. }
  557. if (typeof client.progress !== "undefined") {
  558. searchParams.set("progress", String(client.progress));
  559. }
  560. if (typeof client.overlay !== "undefined") {
  561. const overlayString =
  562. typeof client.overlay === "boolean"
  563. ? String(client.overlay)
  564. : JSON.stringify({
  565. ...client.overlay,
  566. errors: encodeOverlaySettings(client.overlay.errors),
  567. warnings: encodeOverlaySettings(client.overlay.warnings),
  568. runtimeErrors: encodeOverlaySettings(
  569. client.overlay.runtimeErrors,
  570. ),
  571. });
  572. searchParams.set("overlay", overlayString);
  573. }
  574. if (typeof client.reconnect !== "undefined") {
  575. searchParams.set(
  576. "reconnect",
  577. typeof client.reconnect === "number"
  578. ? String(client.reconnect)
  579. : "10",
  580. );
  581. }
  582. if (typeof this.options.hot !== "undefined") {
  583. searchParams.set("hot", String(this.options.hot));
  584. }
  585. if (typeof this.options.liveReload !== "undefined") {
  586. searchParams.set("live-reload", String(this.options.liveReload));
  587. }
  588. webSocketURLStr = searchParams.toString();
  589. }
  590. additionalEntries.push(
  591. `${require.resolve("../client/index.js")}?${webSocketURLStr}`,
  592. );
  593. }
  594. if (this.options.hot === "only") {
  595. additionalEntries.push(require.resolve("webpack/hot/only-dev-server"));
  596. } else if (this.options.hot) {
  597. additionalEntries.push(require.resolve("webpack/hot/dev-server"));
  598. }
  599. const webpack = compiler.webpack || require("webpack");
  600. // use a hook to add entries if available
  601. for (const additionalEntry of additionalEntries) {
  602. new webpack.EntryPlugin(compiler.context, additionalEntry, {
  603. // eslint-disable-next-line no-undefined
  604. name: undefined,
  605. }).apply(compiler);
  606. }
  607. }
  608. /**
  609. * @private
  610. * @returns {Compiler["options"]}
  611. */
  612. getCompilerOptions() {
  613. if (
  614. typeof (/** @type {MultiCompiler} */ (this.compiler).compilers) !==
  615. "undefined"
  616. ) {
  617. if (/** @type {MultiCompiler} */ (this.compiler).compilers.length === 1) {
  618. return (
  619. /** @type {MultiCompiler} */
  620. (this.compiler).compilers[0].options
  621. );
  622. }
  623. // Configuration with the `devServer` options
  624. const compilerWithDevServer =
  625. /** @type {MultiCompiler} */
  626. (this.compiler).compilers.find((config) => config.options.devServer);
  627. if (compilerWithDevServer) {
  628. return compilerWithDevServer.options;
  629. }
  630. // Configuration with `web` preset
  631. const compilerWithWebPreset =
  632. /** @type {MultiCompiler} */
  633. (this.compiler).compilers.find(
  634. (config) =>
  635. (config.options.externalsPresets &&
  636. config.options.externalsPresets.web) ||
  637. [
  638. "web",
  639. "webworker",
  640. "electron-preload",
  641. "electron-renderer",
  642. "node-webkit",
  643. // eslint-disable-next-line no-undefined
  644. undefined,
  645. null,
  646. ].includes(/** @type {string} */ (config.options.target)),
  647. );
  648. if (compilerWithWebPreset) {
  649. return compilerWithWebPreset.options;
  650. }
  651. // Fallback
  652. return /** @type {MultiCompiler} */ (this.compiler).compilers[0].options;
  653. }
  654. return /** @type {Compiler} */ (this.compiler).options;
  655. }
  656. /**
  657. * @private
  658. * @returns {Promise<void>}
  659. */
  660. async normalizeOptions() {
  661. const { options } = this;
  662. const compilerOptions = this.getCompilerOptions();
  663. const compilerWatchOptions = compilerOptions.watchOptions;
  664. /**
  665. * @param {WatchOptions & { aggregateTimeout?: number, ignored?: WatchOptions["ignored"], poll?: number | boolean }} watchOptions
  666. * @returns {WatchOptions}
  667. */
  668. const getWatchOptions = (watchOptions = {}) => {
  669. const getPolling = () => {
  670. if (typeof watchOptions.usePolling !== "undefined") {
  671. return watchOptions.usePolling;
  672. }
  673. if (typeof watchOptions.poll !== "undefined") {
  674. return Boolean(watchOptions.poll);
  675. }
  676. if (typeof compilerWatchOptions.poll !== "undefined") {
  677. return Boolean(compilerWatchOptions.poll);
  678. }
  679. return false;
  680. };
  681. const getInterval = () => {
  682. if (typeof watchOptions.interval !== "undefined") {
  683. return watchOptions.interval;
  684. }
  685. if (typeof watchOptions.poll === "number") {
  686. return watchOptions.poll;
  687. }
  688. if (typeof compilerWatchOptions.poll === "number") {
  689. return compilerWatchOptions.poll;
  690. }
  691. };
  692. const usePolling = getPolling();
  693. const interval = getInterval();
  694. const { poll, ...rest } = watchOptions;
  695. return {
  696. ignoreInitial: true,
  697. persistent: true,
  698. followSymlinks: false,
  699. atomic: false,
  700. alwaysStat: true,
  701. ignorePermissionErrors: true,
  702. // Respect options from compiler watchOptions
  703. usePolling,
  704. interval,
  705. ignored: watchOptions.ignored,
  706. // TODO: we respect these options for all watch options and allow developers to pass them to chokidar, but chokidar doesn't have these options maybe we need revisit that in future
  707. ...rest,
  708. };
  709. };
  710. /**
  711. * @param {string | Static | undefined} [optionsForStatic]
  712. * @returns {NormalizedStatic}
  713. */
  714. const getStaticItem = (optionsForStatic) => {
  715. const getDefaultStaticOptions = () => {
  716. return {
  717. directory: path.join(process.cwd(), "public"),
  718. staticOptions: {},
  719. publicPath: ["/"],
  720. serveIndex: { icons: true },
  721. watch: getWatchOptions(),
  722. };
  723. };
  724. /** @type {NormalizedStatic} */
  725. let item;
  726. if (typeof optionsForStatic === "undefined") {
  727. item = getDefaultStaticOptions();
  728. } else if (typeof optionsForStatic === "string") {
  729. item = {
  730. ...getDefaultStaticOptions(),
  731. directory: optionsForStatic,
  732. };
  733. } else {
  734. const def = getDefaultStaticOptions();
  735. item = {
  736. directory:
  737. typeof optionsForStatic.directory !== "undefined"
  738. ? optionsForStatic.directory
  739. : def.directory,
  740. staticOptions:
  741. typeof optionsForStatic.staticOptions !== "undefined"
  742. ? { ...def.staticOptions, ...optionsForStatic.staticOptions }
  743. : def.staticOptions,
  744. publicPath:
  745. // eslint-disable-next-line no-nested-ternary
  746. typeof optionsForStatic.publicPath !== "undefined"
  747. ? Array.isArray(optionsForStatic.publicPath)
  748. ? optionsForStatic.publicPath
  749. : [optionsForStatic.publicPath]
  750. : def.publicPath,
  751. serveIndex:
  752. // Check if 'serveIndex' property is defined in 'optionsForStatic'
  753. // If 'serveIndex' is a boolean and true, use default 'serveIndex'
  754. // If 'serveIndex' is an object, merge its properties with default 'serveIndex'
  755. // If 'serveIndex' is neither a boolean true nor an object, use it as-is
  756. // If 'serveIndex' is not defined in 'optionsForStatic', use default 'serveIndex'
  757. // eslint-disable-next-line no-nested-ternary
  758. typeof optionsForStatic.serveIndex !== "undefined"
  759. ? // eslint-disable-next-line no-nested-ternary
  760. typeof optionsForStatic.serveIndex === "boolean" &&
  761. optionsForStatic.serveIndex
  762. ? def.serveIndex
  763. : typeof optionsForStatic.serveIndex === "object"
  764. ? { ...def.serveIndex, ...optionsForStatic.serveIndex }
  765. : optionsForStatic.serveIndex
  766. : def.serveIndex,
  767. watch:
  768. // eslint-disable-next-line no-nested-ternary
  769. typeof optionsForStatic.watch !== "undefined"
  770. ? // eslint-disable-next-line no-nested-ternary
  771. typeof optionsForStatic.watch === "boolean"
  772. ? optionsForStatic.watch
  773. ? def.watch
  774. : false
  775. : getWatchOptions(optionsForStatic.watch)
  776. : def.watch,
  777. };
  778. }
  779. if (Server.isAbsoluteURL(item.directory)) {
  780. throw new Error("Using a URL as static.directory is not supported");
  781. }
  782. return item;
  783. };
  784. if (typeof options.allowedHosts === "undefined") {
  785. // AllowedHosts allows some default hosts picked from `options.host` or `webSocketURL.hostname` and `localhost`
  786. options.allowedHosts = "auto";
  787. }
  788. // We store allowedHosts as array when supplied as string
  789. else if (
  790. typeof options.allowedHosts === "string" &&
  791. options.allowedHosts !== "auto" &&
  792. options.allowedHosts !== "all"
  793. ) {
  794. options.allowedHosts = [options.allowedHosts];
  795. }
  796. // CLI pass options as array, we should normalize them
  797. else if (
  798. Array.isArray(options.allowedHosts) &&
  799. options.allowedHosts.includes("all")
  800. ) {
  801. options.allowedHosts = "all";
  802. }
  803. if (typeof options.bonjour === "undefined") {
  804. options.bonjour = false;
  805. } else if (typeof options.bonjour === "boolean") {
  806. options.bonjour = options.bonjour ? {} : false;
  807. }
  808. if (
  809. typeof options.client === "undefined" ||
  810. (typeof options.client === "object" && options.client !== null)
  811. ) {
  812. if (!options.client) {
  813. options.client = {};
  814. }
  815. if (typeof options.client.webSocketURL === "undefined") {
  816. options.client.webSocketURL = {};
  817. } else if (typeof options.client.webSocketURL === "string") {
  818. const parsedURL = new URL(options.client.webSocketURL);
  819. options.client.webSocketURL = {
  820. protocol: parsedURL.protocol,
  821. hostname: parsedURL.hostname,
  822. port: parsedURL.port.length > 0 ? Number(parsedURL.port) : "",
  823. pathname: parsedURL.pathname,
  824. username: parsedURL.username,
  825. password: parsedURL.password,
  826. };
  827. } else if (typeof options.client.webSocketURL.port === "string") {
  828. options.client.webSocketURL.port = Number(
  829. options.client.webSocketURL.port,
  830. );
  831. }
  832. // Enable client overlay by default
  833. if (typeof options.client.overlay === "undefined") {
  834. options.client.overlay = true;
  835. } else if (typeof options.client.overlay !== "boolean") {
  836. options.client.overlay = {
  837. errors: true,
  838. warnings: true,
  839. ...options.client.overlay,
  840. };
  841. }
  842. if (typeof options.client.reconnect === "undefined") {
  843. options.client.reconnect = 10;
  844. } else if (options.client.reconnect === true) {
  845. options.client.reconnect = Infinity;
  846. } else if (options.client.reconnect === false) {
  847. options.client.reconnect = 0;
  848. }
  849. // Respect infrastructureLogging.level
  850. if (typeof options.client.logging === "undefined") {
  851. options.client.logging = compilerOptions.infrastructureLogging
  852. ? compilerOptions.infrastructureLogging.level
  853. : "info";
  854. }
  855. }
  856. if (typeof options.compress === "undefined") {
  857. options.compress = true;
  858. }
  859. if (typeof options.devMiddleware === "undefined") {
  860. options.devMiddleware = {};
  861. }
  862. // No need to normalize `headers`
  863. if (typeof options.historyApiFallback === "undefined") {
  864. options.historyApiFallback = false;
  865. } else if (
  866. typeof options.historyApiFallback === "boolean" &&
  867. options.historyApiFallback
  868. ) {
  869. options.historyApiFallback = {};
  870. }
  871. // No need to normalize `host`
  872. options.hot =
  873. typeof options.hot === "boolean" || options.hot === "only"
  874. ? options.hot
  875. : true;
  876. options.server = {
  877. type:
  878. // eslint-disable-next-line no-nested-ternary
  879. typeof options.server === "string"
  880. ? options.server
  881. : typeof (options.server || {}).type === "string"
  882. ? /** @type {ServerConfiguration} */ (options.server).type || "http"
  883. : "http",
  884. options: {
  885. .../** @type {ServerOptions} */ (options.https),
  886. .../** @type {ServerConfiguration} */ (options.server || {}).options,
  887. },
  888. };
  889. if (
  890. options.server.type === "spdy" &&
  891. typeof (/** @type {ServerOptions} */ (options.server.options).spdy) ===
  892. "undefined"
  893. ) {
  894. /** @type {ServerOptions} */
  895. (options.server.options).spdy = {
  896. protocols: ["h2", "http/1.1"],
  897. };
  898. }
  899. if (options.server.type === "https" || options.server.type === "spdy") {
  900. if (
  901. typeof (
  902. /** @type {ServerOptions} */ (options.server.options).requestCert
  903. ) === "undefined"
  904. ) {
  905. /** @type {ServerOptions} */
  906. (options.server.options).requestCert = false;
  907. }
  908. const httpsProperties =
  909. /** @type {Array<keyof ServerOptions>} */
  910. (["ca", "cert", "crl", "key", "pfx"]);
  911. for (const property of httpsProperties) {
  912. if (
  913. typeof (
  914. /** @type {ServerOptions} */ (options.server.options)[property]
  915. ) === "undefined"
  916. ) {
  917. // eslint-disable-next-line no-continue
  918. continue;
  919. }
  920. /** @type {any} */
  921. const value =
  922. /** @type {ServerOptions} */
  923. (options.server.options)[property];
  924. /**
  925. * @param {string | Buffer | undefined} item
  926. * @returns {string | Buffer | undefined}
  927. */
  928. const readFile = (item) => {
  929. if (
  930. Buffer.isBuffer(item) ||
  931. (typeof item === "object" && item !== null && !Array.isArray(item))
  932. ) {
  933. return item;
  934. }
  935. if (item) {
  936. let stats = null;
  937. try {
  938. stats = fs.lstatSync(fs.realpathSync(item)).isFile();
  939. } catch (error) {
  940. // Ignore error
  941. }
  942. // It is a file
  943. return stats ? fs.readFileSync(item) : item;
  944. }
  945. };
  946. /** @type {any} */
  947. (options.server.options)[property] = Array.isArray(value)
  948. ? value.map((item) => readFile(item))
  949. : readFile(value);
  950. }
  951. let fakeCert;
  952. if (
  953. !(/** @type {ServerOptions} */ (options.server.options).key) ||
  954. !(/** @type {ServerOptions} */ (options.server.options).cert)
  955. ) {
  956. const certificateDir = Server.findCacheDir();
  957. const certificatePath = path.join(certificateDir, "server.pem");
  958. let certificateExists;
  959. try {
  960. const certificate = await fs.promises.stat(certificatePath);
  961. certificateExists = certificate.isFile();
  962. } catch {
  963. certificateExists = false;
  964. }
  965. if (certificateExists) {
  966. const certificateTtl = 1000 * 60 * 60 * 24;
  967. const certificateStat = await fs.promises.stat(certificatePath);
  968. const now = Number(new Date());
  969. // cert is more than 30 days old, kill it with fire
  970. if ((now - Number(certificateStat.ctime)) / certificateTtl > 30) {
  971. const { rimraf } = require("rimraf");
  972. this.logger.info(
  973. "SSL certificate is more than 30 days old. Removing...",
  974. );
  975. await rimraf(certificatePath);
  976. certificateExists = false;
  977. }
  978. }
  979. if (!certificateExists) {
  980. this.logger.info("Generating SSL certificate...");
  981. // @ts-ignore
  982. const selfsigned = require("selfsigned");
  983. const attributes = [{ name: "commonName", value: "localhost" }];
  984. const pems = selfsigned.generate(attributes, {
  985. algorithm: "sha256",
  986. days: 30,
  987. keySize: 2048,
  988. extensions: [
  989. {
  990. name: "basicConstraints",
  991. cA: true,
  992. },
  993. {
  994. name: "keyUsage",
  995. keyCertSign: true,
  996. digitalSignature: true,
  997. nonRepudiation: true,
  998. keyEncipherment: true,
  999. dataEncipherment: true,
  1000. },
  1001. {
  1002. name: "extKeyUsage",
  1003. serverAuth: true,
  1004. clientAuth: true,
  1005. codeSigning: true,
  1006. timeStamping: true,
  1007. },
  1008. {
  1009. name: "subjectAltName",
  1010. altNames: [
  1011. {
  1012. // type 2 is DNS
  1013. type: 2,
  1014. value: "localhost",
  1015. },
  1016. {
  1017. type: 2,
  1018. value: "localhost.localdomain",
  1019. },
  1020. {
  1021. type: 2,
  1022. value: "lvh.me",
  1023. },
  1024. {
  1025. type: 2,
  1026. value: "*.lvh.me",
  1027. },
  1028. {
  1029. type: 2,
  1030. value: "[::1]",
  1031. },
  1032. {
  1033. // type 7 is IP
  1034. type: 7,
  1035. ip: "127.0.0.1",
  1036. },
  1037. {
  1038. type: 7,
  1039. ip: "fe80::1",
  1040. },
  1041. ],
  1042. },
  1043. ],
  1044. });
  1045. await fs.promises.mkdir(certificateDir, { recursive: true });
  1046. await fs.promises.writeFile(
  1047. certificatePath,
  1048. pems.private + pems.cert,
  1049. {
  1050. encoding: "utf8",
  1051. },
  1052. );
  1053. }
  1054. fakeCert = await fs.promises.readFile(certificatePath);
  1055. this.logger.info(`SSL certificate: ${certificatePath}`);
  1056. }
  1057. /** @type {ServerOptions} */
  1058. (options.server.options).key =
  1059. /** @type {ServerOptions} */
  1060. (options.server.options).key || fakeCert;
  1061. /** @type {ServerOptions} */
  1062. (options.server.options).cert =
  1063. /** @type {ServerOptions} */
  1064. (options.server.options).cert || fakeCert;
  1065. }
  1066. if (typeof options.ipc === "boolean") {
  1067. const isWindows = process.platform === "win32";
  1068. const pipePrefix = isWindows ? "\\\\.\\pipe\\" : os.tmpdir();
  1069. const pipeName = "webpack-dev-server.sock";
  1070. options.ipc = path.join(pipePrefix, pipeName);
  1071. }
  1072. options.liveReload =
  1073. typeof options.liveReload !== "undefined" ? options.liveReload : true;
  1074. // https://github.com/webpack/webpack-dev-server/issues/1990
  1075. const defaultOpenOptions = { wait: false };
  1076. /**
  1077. * @param {any} target
  1078. * @returns {NormalizedOpen[]}
  1079. */
  1080. const getOpenItemsFromObject = ({ target, ...rest }) => {
  1081. const normalizedOptions = { ...defaultOpenOptions, ...rest };
  1082. if (typeof normalizedOptions.app === "string") {
  1083. normalizedOptions.app = {
  1084. name: normalizedOptions.app,
  1085. };
  1086. }
  1087. const normalizedTarget = typeof target === "undefined" ? "<url>" : target;
  1088. if (Array.isArray(normalizedTarget)) {
  1089. return normalizedTarget.map((singleTarget) => {
  1090. return { target: singleTarget, options: normalizedOptions };
  1091. });
  1092. }
  1093. return [{ target: normalizedTarget, options: normalizedOptions }];
  1094. };
  1095. if (typeof options.open === "undefined") {
  1096. /** @type {NormalizedOpen[]} */
  1097. (options.open) = [];
  1098. } else if (typeof options.open === "boolean") {
  1099. /** @type {NormalizedOpen[]} */
  1100. (options.open) = options.open
  1101. ? [
  1102. {
  1103. target: "<url>",
  1104. options: /** @type {OpenOptions} */ (defaultOpenOptions),
  1105. },
  1106. ]
  1107. : [];
  1108. } else if (typeof options.open === "string") {
  1109. /** @type {NormalizedOpen[]} */
  1110. (options.open) = [{ target: options.open, options: defaultOpenOptions }];
  1111. } else if (Array.isArray(options.open)) {
  1112. /**
  1113. * @type {NormalizedOpen[]}
  1114. */
  1115. const result = [];
  1116. options.open.forEach((item) => {
  1117. if (typeof item === "string") {
  1118. result.push({ target: item, options: defaultOpenOptions });
  1119. return;
  1120. }
  1121. result.push(...getOpenItemsFromObject(item));
  1122. });
  1123. /** @type {NormalizedOpen[]} */
  1124. (options.open) = result;
  1125. } else {
  1126. /** @type {NormalizedOpen[]} */
  1127. (options.open) = [...getOpenItemsFromObject(options.open)];
  1128. }
  1129. if (typeof options.port === "string" && options.port !== "auto") {
  1130. options.port = Number(options.port);
  1131. }
  1132. /**
  1133. * Assume a proxy configuration specified as:
  1134. * proxy: {
  1135. * 'context': { options }
  1136. * }
  1137. * OR
  1138. * proxy: {
  1139. * 'context': 'target'
  1140. * }
  1141. */
  1142. if (typeof options.proxy !== "undefined") {
  1143. options.proxy = options.proxy.map((item) => {
  1144. if (typeof item === "function") {
  1145. return item;
  1146. }
  1147. /**
  1148. * @param {"info" | "warn" | "error" | "debug" | "silent" | undefined | "none" | "log" | "verbose"} level
  1149. * @returns {"info" | "warn" | "error" | "debug" | "silent" | undefined}
  1150. */
  1151. const getLogLevelForProxy = (level) => {
  1152. if (level === "none") {
  1153. return "silent";
  1154. }
  1155. if (level === "log") {
  1156. return "info";
  1157. }
  1158. if (level === "verbose") {
  1159. return "debug";
  1160. }
  1161. return level;
  1162. };
  1163. if (typeof item.logLevel === "undefined") {
  1164. item.logLevel = getLogLevelForProxy(
  1165. compilerOptions.infrastructureLogging
  1166. ? compilerOptions.infrastructureLogging.level
  1167. : "info",
  1168. );
  1169. }
  1170. if (typeof item.logProvider === "undefined") {
  1171. item.logProvider = () => this.logger;
  1172. }
  1173. return item;
  1174. });
  1175. }
  1176. if (typeof options.setupExitSignals === "undefined") {
  1177. options.setupExitSignals = true;
  1178. }
  1179. if (typeof options.static === "undefined") {
  1180. options.static = [getStaticItem()];
  1181. } else if (typeof options.static === "boolean") {
  1182. options.static = options.static ? [getStaticItem()] : false;
  1183. } else if (typeof options.static === "string") {
  1184. options.static = [getStaticItem(options.static)];
  1185. } else if (Array.isArray(options.static)) {
  1186. options.static = options.static.map((item) => getStaticItem(item));
  1187. } else {
  1188. options.static = [getStaticItem(options.static)];
  1189. }
  1190. if (typeof options.watchFiles === "string") {
  1191. options.watchFiles = [
  1192. { paths: options.watchFiles, options: getWatchOptions() },
  1193. ];
  1194. } else if (
  1195. typeof options.watchFiles === "object" &&
  1196. options.watchFiles !== null &&
  1197. !Array.isArray(options.watchFiles)
  1198. ) {
  1199. options.watchFiles = [
  1200. {
  1201. paths: options.watchFiles.paths,
  1202. options: getWatchOptions(options.watchFiles.options || {}),
  1203. },
  1204. ];
  1205. } else if (Array.isArray(options.watchFiles)) {
  1206. options.watchFiles = options.watchFiles.map((item) => {
  1207. if (typeof item === "string") {
  1208. return { paths: item, options: getWatchOptions() };
  1209. }
  1210. return {
  1211. paths: item.paths,
  1212. options: getWatchOptions(item.options || {}),
  1213. };
  1214. });
  1215. } else {
  1216. options.watchFiles = [];
  1217. }
  1218. const defaultWebSocketServerType = "ws";
  1219. const defaultWebSocketServerOptions = { path: "/ws" };
  1220. if (typeof options.webSocketServer === "undefined") {
  1221. options.webSocketServer = {
  1222. type: defaultWebSocketServerType,
  1223. options: defaultWebSocketServerOptions,
  1224. };
  1225. } else if (
  1226. typeof options.webSocketServer === "boolean" &&
  1227. !options.webSocketServer
  1228. ) {
  1229. options.webSocketServer = false;
  1230. } else if (
  1231. typeof options.webSocketServer === "string" ||
  1232. typeof options.webSocketServer === "function"
  1233. ) {
  1234. options.webSocketServer = {
  1235. type: options.webSocketServer,
  1236. options: defaultWebSocketServerOptions,
  1237. };
  1238. } else {
  1239. options.webSocketServer = {
  1240. type:
  1241. /** @type {WebSocketServerConfiguration} */
  1242. (options.webSocketServer).type || defaultWebSocketServerType,
  1243. options: {
  1244. ...defaultWebSocketServerOptions,
  1245. .../** @type {WebSocketServerConfiguration} */
  1246. (options.webSocketServer).options,
  1247. },
  1248. };
  1249. const webSocketServer =
  1250. /** @type {{ type: WebSocketServerConfiguration["type"], options: NonNullable<WebSocketServerConfiguration["options"]> }} */
  1251. (options.webSocketServer);
  1252. if (typeof webSocketServer.options.port === "string") {
  1253. webSocketServer.options.port = Number(webSocketServer.options.port);
  1254. }
  1255. }
  1256. }
  1257. /**
  1258. * @private
  1259. * @returns {string}
  1260. */
  1261. getClientTransport() {
  1262. let clientImplementation;
  1263. let clientImplementationFound = true;
  1264. const isKnownWebSocketServerImplementation =
  1265. this.options.webSocketServer &&
  1266. typeof (
  1267. /** @type {WebSocketServerConfiguration} */
  1268. (this.options.webSocketServer).type
  1269. ) === "string" &&
  1270. // @ts-ignore
  1271. (this.options.webSocketServer.type === "ws" ||
  1272. /** @type {WebSocketServerConfiguration} */
  1273. (this.options.webSocketServer).type === "sockjs");
  1274. let clientTransport;
  1275. if (this.options.client) {
  1276. if (
  1277. typeof (
  1278. /** @type {ClientConfiguration} */
  1279. (this.options.client).webSocketTransport
  1280. ) !== "undefined"
  1281. ) {
  1282. clientTransport =
  1283. /** @type {ClientConfiguration} */
  1284. (this.options.client).webSocketTransport;
  1285. } else if (isKnownWebSocketServerImplementation) {
  1286. clientTransport =
  1287. /** @type {WebSocketServerConfiguration} */
  1288. (this.options.webSocketServer).type;
  1289. } else {
  1290. clientTransport = "ws";
  1291. }
  1292. } else {
  1293. clientTransport = "ws";
  1294. }
  1295. switch (typeof clientTransport) {
  1296. case "string":
  1297. // could be 'sockjs', 'ws', or a path that should be required
  1298. if (clientTransport === "sockjs") {
  1299. clientImplementation = require.resolve(
  1300. "../client/clients/SockJSClient",
  1301. );
  1302. } else if (clientTransport === "ws") {
  1303. clientImplementation = require.resolve(
  1304. "../client/clients/WebSocketClient",
  1305. );
  1306. } else {
  1307. try {
  1308. clientImplementation = require.resolve(clientTransport);
  1309. } catch (e) {
  1310. clientImplementationFound = false;
  1311. }
  1312. }
  1313. break;
  1314. default:
  1315. clientImplementationFound = false;
  1316. }
  1317. if (!clientImplementationFound) {
  1318. throw new Error(
  1319. `${
  1320. !isKnownWebSocketServerImplementation
  1321. ? "When you use custom web socket implementation you must explicitly specify client.webSocketTransport. "
  1322. : ""
  1323. }client.webSocketTransport must be a string denoting a default implementation (e.g. 'sockjs', 'ws') or a full path to a JS file via require.resolve(...) which exports a class `,
  1324. );
  1325. }
  1326. return /** @type {string} */ (clientImplementation);
  1327. }
  1328. /**
  1329. * @private
  1330. * @returns {string}
  1331. */
  1332. getServerTransport() {
  1333. let implementation;
  1334. let implementationFound = true;
  1335. switch (
  1336. typeof (
  1337. /** @type {WebSocketServerConfiguration} */
  1338. (this.options.webSocketServer).type
  1339. )
  1340. ) {
  1341. case "string":
  1342. // Could be 'sockjs', in the future 'ws', or a path that should be required
  1343. if (
  1344. /** @type {WebSocketServerConfiguration} */ (
  1345. this.options.webSocketServer
  1346. ).type === "sockjs"
  1347. ) {
  1348. implementation = require("./servers/SockJSServer");
  1349. } else if (
  1350. /** @type {WebSocketServerConfiguration} */ (
  1351. this.options.webSocketServer
  1352. ).type === "ws"
  1353. ) {
  1354. implementation = require("./servers/WebsocketServer");
  1355. } else {
  1356. try {
  1357. // eslint-disable-next-line import/no-dynamic-require
  1358. implementation = require(
  1359. /** @type {WebSocketServerConfiguration} */ (
  1360. this.options.webSocketServer
  1361. ).type,
  1362. );
  1363. } catch (error) {
  1364. implementationFound = false;
  1365. }
  1366. }
  1367. break;
  1368. case "function":
  1369. implementation = /** @type {WebSocketServerConfiguration} */ (
  1370. this.options.webSocketServer
  1371. ).type;
  1372. break;
  1373. default:
  1374. implementationFound = false;
  1375. }
  1376. if (!implementationFound) {
  1377. throw new Error(
  1378. "webSocketServer (webSocketServer.type) must be a string denoting a default implementation (e.g. 'ws', 'sockjs'), a full path to " +
  1379. "a JS file which exports a class extending BaseServer (webpack-dev-server/lib/servers/BaseServer.js) " +
  1380. "via require.resolve(...), or the class itself which extends BaseServer",
  1381. );
  1382. }
  1383. return implementation;
  1384. }
  1385. /**
  1386. * @private
  1387. * @returns {void}
  1388. */
  1389. setupProgressPlugin() {
  1390. const { ProgressPlugin } =
  1391. /** @type {MultiCompiler}*/
  1392. (this.compiler).compilers
  1393. ? /** @type {MultiCompiler}*/ (this.compiler).compilers[0].webpack
  1394. : /** @type {Compiler}*/ (this.compiler).webpack;
  1395. new ProgressPlugin(
  1396. /**
  1397. * @param {number} percent
  1398. * @param {string} msg
  1399. * @param {string} addInfo
  1400. * @param {string} pluginName
  1401. */
  1402. (percent, msg, addInfo, pluginName) => {
  1403. percent = Math.floor(percent * 100);
  1404. if (percent === 100) {
  1405. msg = "Compilation completed";
  1406. }
  1407. if (addInfo) {
  1408. msg = `${msg} (${addInfo})`;
  1409. }
  1410. if (this.webSocketServer) {
  1411. this.sendMessage(this.webSocketServer.clients, "progress-update", {
  1412. percent,
  1413. msg,
  1414. pluginName,
  1415. });
  1416. }
  1417. if (this.server) {
  1418. this.server.emit("progress-update", { percent, msg, pluginName });
  1419. }
  1420. },
  1421. ).apply(this.compiler);
  1422. }
  1423. /**
  1424. * @private
  1425. * @returns {Promise<void>}
  1426. */
  1427. async initialize() {
  1428. if (this.options.webSocketServer) {
  1429. const compilers =
  1430. /** @type {MultiCompiler} */
  1431. (this.compiler).compilers || [this.compiler];
  1432. compilers.forEach((compiler) => {
  1433. this.addAdditionalEntries(compiler);
  1434. const webpack = compiler.webpack || require("webpack");
  1435. new webpack.ProvidePlugin({
  1436. __webpack_dev_server_client__: this.getClientTransport(),
  1437. }).apply(compiler);
  1438. if (this.options.hot) {
  1439. const HMRPluginExists = compiler.options.plugins.find(
  1440. (p) => p && p.constructor === webpack.HotModuleReplacementPlugin,
  1441. );
  1442. if (HMRPluginExists) {
  1443. this.logger.warn(
  1444. `"hot: true" automatically applies HMR plugin, you don't have to add it manually to your webpack configuration.`,
  1445. );
  1446. } else {
  1447. // Apply the HMR plugin
  1448. const plugin = new webpack.HotModuleReplacementPlugin();
  1449. plugin.apply(compiler);
  1450. }
  1451. }
  1452. });
  1453. if (
  1454. this.options.client &&
  1455. /** @type {ClientConfiguration} */ (this.options.client).progress
  1456. ) {
  1457. this.setupProgressPlugin();
  1458. }
  1459. }
  1460. this.setupHooks();
  1461. this.setupApp();
  1462. this.setupHostHeaderCheck();
  1463. this.setupDevMiddleware();
  1464. // Should be after `webpack-dev-middleware`, otherwise other middlewares might rewrite response
  1465. this.setupBuiltInRoutes();
  1466. this.setupWatchFiles();
  1467. this.setupWatchStaticFiles();
  1468. this.setupMiddlewares();
  1469. this.createServer();
  1470. if (this.options.setupExitSignals) {
  1471. const signals = ["SIGINT", "SIGTERM"];
  1472. let needForceShutdown = false;
  1473. signals.forEach((signal) => {
  1474. const listener = () => {
  1475. if (needForceShutdown) {
  1476. process.exit();
  1477. }
  1478. this.logger.info(
  1479. "Gracefully shutting down. To force exit, press ^C again. Please wait...",
  1480. );
  1481. needForceShutdown = true;
  1482. this.stopCallback(() => {
  1483. if (typeof this.compiler.close === "function") {
  1484. this.compiler.close(() => {
  1485. process.exit();
  1486. });
  1487. } else {
  1488. process.exit();
  1489. }
  1490. });
  1491. };
  1492. this.listeners.push({ name: signal, listener });
  1493. process.on(signal, listener);
  1494. });
  1495. }
  1496. // Proxy WebSocket without the initial http request
  1497. // https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade
  1498. /** @type {RequestHandler[]} */
  1499. (this.webSocketProxies).forEach((webSocketProxy) => {
  1500. /** @type {import("http").Server} */
  1501. (this.server).on(
  1502. "upgrade",
  1503. /** @type {RequestHandler & { upgrade: NonNullable<RequestHandler["upgrade"]> }} */
  1504. (webSocketProxy).upgrade,
  1505. );
  1506. }, this);
  1507. }
  1508. /**
  1509. * @private
  1510. * @returns {void}
  1511. */
  1512. setupApp() {
  1513. /** @type {import("express").Application | undefined}*/
  1514. this.app = new /** @type {any} */ (getExpress())();
  1515. }
  1516. /**
  1517. * @private
  1518. * @param {Stats | MultiStats} statsObj
  1519. * @returns {StatsCompilation}
  1520. */
  1521. getStats(statsObj) {
  1522. const stats = Server.DEFAULT_STATS;
  1523. const compilerOptions = this.getCompilerOptions();
  1524. // @ts-ignore
  1525. if (compilerOptions.stats && compilerOptions.stats.warningsFilter) {
  1526. // @ts-ignore
  1527. stats.warningsFilter = compilerOptions.stats.warningsFilter;
  1528. }
  1529. return statsObj.toJson(stats);
  1530. }
  1531. /**
  1532. * @private
  1533. * @returns {void}
  1534. */
  1535. setupHooks() {
  1536. this.compiler.hooks.invalid.tap("webpack-dev-server", () => {
  1537. if (this.webSocketServer) {
  1538. this.sendMessage(this.webSocketServer.clients, "invalid");
  1539. }
  1540. });
  1541. this.compiler.hooks.done.tap(
  1542. "webpack-dev-server",
  1543. /**
  1544. * @param {Stats | MultiStats} stats
  1545. */
  1546. (stats) => {
  1547. if (this.webSocketServer) {
  1548. this.sendStats(this.webSocketServer.clients, this.getStats(stats));
  1549. }
  1550. /**
  1551. * @private
  1552. * @type {Stats | MultiStats}
  1553. */
  1554. this.stats = stats;
  1555. },
  1556. );
  1557. }
  1558. /**
  1559. * @private
  1560. * @returns {void}
  1561. */
  1562. setupHostHeaderCheck() {
  1563. /** @type {import("express").Application} */
  1564. (this.app).all(
  1565. "*",
  1566. /**
  1567. * @param {Request} req
  1568. * @param {Response} res
  1569. * @param {NextFunction} next
  1570. * @returns {void}
  1571. */
  1572. (req, res, next) => {
  1573. if (
  1574. this.checkHeader(
  1575. /** @type {{ [key: string]: string | undefined }} */
  1576. (req.headers),
  1577. "host",
  1578. )
  1579. ) {
  1580. return next();
  1581. }
  1582. res.send("Invalid Host header");
  1583. },
  1584. );
  1585. }
  1586. /**
  1587. * @private
  1588. * @returns {void}
  1589. */
  1590. setupDevMiddleware() {
  1591. const webpackDevMiddleware = require("webpack-dev-middleware");
  1592. // middleware for serving webpack bundle
  1593. this.middleware = webpackDevMiddleware(
  1594. this.compiler,
  1595. this.options.devMiddleware,
  1596. );
  1597. }
  1598. /**
  1599. * @private
  1600. * @returns {void}
  1601. */
  1602. setupBuiltInRoutes() {
  1603. const { app, middleware } = this;
  1604. /** @type {import("express").Application} */
  1605. (app).get("/__webpack_dev_server__/sockjs.bundle.js", (req, res) => {
  1606. res.setHeader("Content-Type", "application/javascript");
  1607. const clientPath = path.join(__dirname, "..", "client");
  1608. res.sendFile(path.join(clientPath, "modules/sockjs-client/index.js"));
  1609. });
  1610. /** @type {import("express").Application} */
  1611. (app).get("/webpack-dev-server/invalidate", (_req, res) => {
  1612. this.invalidate();
  1613. res.end();
  1614. });
  1615. /** @type {import("express").Application} */
  1616. (app).get("/webpack-dev-server/open-editor", (req, res) => {
  1617. const fileName = req.query.fileName;
  1618. if (typeof fileName === "string") {
  1619. // @ts-ignore
  1620. const launchEditor = require("launch-editor");
  1621. launchEditor(fileName);
  1622. }
  1623. res.end();
  1624. });
  1625. /** @type {import("express").Application} */
  1626. (app).get("/webpack-dev-server", (req, res) => {
  1627. /** @type {import("webpack-dev-middleware").API<Request, Response>}*/
  1628. (middleware).waitUntilValid((stats) => {
  1629. res.setHeader("Content-Type", "text/html");
  1630. // HEAD requests should not return body content
  1631. if (req.method === "HEAD") {
  1632. res.end();
  1633. return;
  1634. }
  1635. res.write(
  1636. '<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>',
  1637. );
  1638. const statsForPrint =
  1639. typeof (/** @type {MultiStats} */ (stats).stats) !== "undefined"
  1640. ? /** @type {MultiStats} */ (stats).toJson().children
  1641. : [/** @type {Stats} */ (stats).toJson()];
  1642. res.write(`<h1>Assets Report:</h1>`);
  1643. /**
  1644. * @type {StatsCompilation[]}
  1645. */
  1646. (statsForPrint).forEach((item, index) => {
  1647. res.write("<div>");
  1648. const name =
  1649. // eslint-disable-next-line no-nested-ternary
  1650. typeof item.name !== "undefined"
  1651. ? item.name
  1652. : /** @type {MultiStats} */ (stats).stats
  1653. ? `unnamed[${index}]`
  1654. : "unnamed";
  1655. res.write(`<h2>Compilation: ${name}</h2>`);
  1656. res.write("<ul>");
  1657. const publicPath = item.publicPath === "auto" ? "" : item.publicPath;
  1658. for (const asset of /** @type {NonNullable<StatsCompilation["assets"]>} */ (
  1659. item.assets
  1660. )) {
  1661. const assetName = asset.name;
  1662. const assetURL = `${publicPath}${assetName}`;
  1663. res.write(
  1664. `<li>
  1665. <strong><a href="${assetURL}" target="_blank">${assetName}</a></strong>
  1666. </li>`,
  1667. );
  1668. }
  1669. res.write("</ul>");
  1670. res.write("</div>");
  1671. });
  1672. res.end("</body></html>");
  1673. });
  1674. });
  1675. }
  1676. /**
  1677. * @private
  1678. * @returns {void}
  1679. */
  1680. setupWatchStaticFiles() {
  1681. if (/** @type {NormalizedStatic[]} */ (this.options.static).length > 0) {
  1682. /** @type {NormalizedStatic[]} */
  1683. (this.options.static).forEach((staticOption) => {
  1684. if (staticOption.watch) {
  1685. this.watchFiles(staticOption.directory, staticOption.watch);
  1686. }
  1687. });
  1688. }
  1689. }
  1690. /**
  1691. * @private
  1692. * @returns {void}
  1693. */
  1694. setupWatchFiles() {
  1695. const { watchFiles } = this.options;
  1696. if (/** @type {WatchFiles[]} */ (watchFiles).length > 0) {
  1697. /** @type {WatchFiles[]} */
  1698. (watchFiles).forEach((item) => {
  1699. this.watchFiles(item.paths, item.options);
  1700. });
  1701. }
  1702. }
  1703. /**
  1704. * @private
  1705. * @returns {void}
  1706. */
  1707. setupMiddlewares() {
  1708. /**
  1709. * @type {Array<Middleware>}
  1710. */
  1711. let middlewares = [];
  1712. // compress is placed last and uses unshift so that it will be the first middleware used
  1713. if (this.options.compress) {
  1714. const compression = require("compression");
  1715. middlewares.push({ name: "compression", middleware: compression() });
  1716. }
  1717. if (typeof this.options.headers !== "undefined") {
  1718. middlewares.push({
  1719. name: "set-headers",
  1720. path: "*",
  1721. middleware: this.setHeaders.bind(this),
  1722. });
  1723. }
  1724. middlewares.push({
  1725. name: "webpack-dev-middleware",
  1726. middleware:
  1727. /** @type {import("webpack-dev-middleware").Middleware<Request, Response>}*/
  1728. (this.middleware),
  1729. });
  1730. if (this.options.proxy) {
  1731. const { createProxyMiddleware } = require("http-proxy-middleware");
  1732. /**
  1733. * @param {ProxyConfigArrayItem} proxyConfig
  1734. * @returns {RequestHandler | undefined}
  1735. */
  1736. const getProxyMiddleware = (proxyConfig) => {
  1737. // It is possible to use the `bypass` method without a `target` or `router`.
  1738. // However, the proxy middleware has no use in this case, and will fail to instantiate.
  1739. if (proxyConfig.target) {
  1740. const context = proxyConfig.context || proxyConfig.path;
  1741. return createProxyMiddleware(
  1742. /** @type {string} */ (context),
  1743. proxyConfig,
  1744. );
  1745. }
  1746. if (proxyConfig.router) {
  1747. return createProxyMiddleware(proxyConfig);
  1748. }
  1749. // TODO improve me after drop `bypass` to always generate error when configuration is bad
  1750. if (!proxyConfig.bypass) {
  1751. util.deprecate(
  1752. () => {},
  1753. `Invalid proxy configuration:\n\n${JSON.stringify(proxyConfig, null, 2)}\n\nThe use of proxy object notation as proxy routes has been removed.\nPlease use the 'router' or 'context' options. Read more at https://github.com/chimurai/http-proxy-middleware/tree/v2.0.6#http-proxy-middleware-options`,
  1754. "DEP_WEBPACK_DEV_SERVER_PROXY_ROUTES_ARGUMENT",
  1755. )();
  1756. }
  1757. };
  1758. /**
  1759. * Assume a proxy configuration specified as:
  1760. * proxy: [
  1761. * {
  1762. * context: "value",
  1763. * ...options,
  1764. * },
  1765. * // or:
  1766. * function() {
  1767. * return {
  1768. * context: "context",
  1769. * ...options,
  1770. * };
  1771. * }
  1772. * ]
  1773. */
  1774. this.options.proxy.forEach((proxyConfigOrCallback) => {
  1775. /**
  1776. * @type {RequestHandler}
  1777. */
  1778. let proxyMiddleware;
  1779. let proxyConfig =
  1780. typeof proxyConfigOrCallback === "function"
  1781. ? proxyConfigOrCallback()
  1782. : proxyConfigOrCallback;
  1783. proxyMiddleware =
  1784. /** @type {RequestHandler} */
  1785. (getProxyMiddleware(proxyConfig));
  1786. if (proxyConfig.ws) {
  1787. this.webSocketProxies.push(proxyMiddleware);
  1788. }
  1789. /**
  1790. * @param {Request} req
  1791. * @param {Response} res
  1792. * @param {NextFunction} next
  1793. * @returns {Promise<void>}
  1794. */
  1795. const handler = async (req, res, next) => {
  1796. if (typeof proxyConfigOrCallback === "function") {
  1797. const newProxyConfig = proxyConfigOrCallback(req, res, next);
  1798. if (newProxyConfig !== proxyConfig) {
  1799. proxyConfig = newProxyConfig;
  1800. const socket = req.socket != null ? req.socket : req.connection;
  1801. // @ts-ignore
  1802. const server = socket != null ? socket.server : null;
  1803. if (server) {
  1804. server.removeAllListeners("close");
  1805. }
  1806. proxyMiddleware =
  1807. /** @type {RequestHandler} */
  1808. (getProxyMiddleware(proxyConfig));
  1809. }
  1810. }
  1811. // - Check if we have a bypass function defined
  1812. // - In case the bypass function is defined we'll retrieve the
  1813. // bypassUrl from it otherwise bypassUrl would be null
  1814. // TODO remove in the next major in favor `context` and `router` options
  1815. const isByPassFuncDefined = typeof proxyConfig.bypass === "function";
  1816. if (isByPassFuncDefined) {
  1817. util.deprecate(
  1818. () => {},
  1819. "Using the 'bypass' option is deprecated. Please use the 'router' or 'context' options. Read more at https://github.com/chimurai/http-proxy-middleware/tree/v2.0.6#http-proxy-middleware-options",
  1820. "DEP_WEBPACK_DEV_SERVER_PROXY_BYPASS_ARGUMENT",
  1821. )();
  1822. }
  1823. const bypassUrl = isByPassFuncDefined
  1824. ? await /** @type {ByPass} */ (proxyConfig.bypass)(
  1825. req,
  1826. res,
  1827. proxyConfig,
  1828. )
  1829. : null;
  1830. if (typeof bypassUrl === "boolean") {
  1831. // skip the proxy
  1832. // @ts-ignore
  1833. req.url = null;
  1834. next();
  1835. } else if (typeof bypassUrl === "string") {
  1836. // byPass to that url
  1837. req.url = bypassUrl;
  1838. next();
  1839. } else if (proxyMiddleware) {
  1840. return proxyMiddleware(req, res, next);
  1841. } else {
  1842. next();
  1843. }
  1844. };
  1845. middlewares.push({
  1846. name: "http-proxy-middleware",
  1847. middleware: handler,
  1848. });
  1849. // Also forward error requests to the proxy so it can handle them.
  1850. middlewares.push({
  1851. name: "http-proxy-middleware-error-handler",
  1852. middleware:
  1853. /**
  1854. * @param {Error} error
  1855. * @param {Request} req
  1856. * @param {Response} res
  1857. * @param {NextFunction} next
  1858. * @returns {any}
  1859. */
  1860. (error, req, res, next) => handler(req, res, next),
  1861. });
  1862. });
  1863. middlewares.push({
  1864. name: "webpack-dev-middleware",
  1865. middleware:
  1866. /** @type {import("webpack-dev-middleware").Middleware<Request, Response>}*/
  1867. (this.middleware),
  1868. });
  1869. }
  1870. if (/** @type {NormalizedStatic[]} */ (this.options.static).length > 0) {
  1871. /** @type {NormalizedStatic[]} */
  1872. (this.options.static).forEach((staticOption) => {
  1873. staticOption.publicPath.forEach((publicPath) => {
  1874. middlewares.push({
  1875. name: "express-static",
  1876. path: publicPath,
  1877. middleware: getExpress().static(
  1878. staticOption.directory,
  1879. staticOption.staticOptions,
  1880. ),
  1881. });
  1882. });
  1883. });
  1884. }
  1885. if (this.options.historyApiFallback) {
  1886. const connectHistoryApiFallback = require("connect-history-api-fallback");
  1887. const { historyApiFallback } = this.options;
  1888. if (
  1889. typeof (
  1890. /** @type {ConnectHistoryApiFallbackOptions} */
  1891. (historyApiFallback).logger
  1892. ) === "undefined" &&
  1893. !(
  1894. /** @type {ConnectHistoryApiFallbackOptions} */
  1895. (historyApiFallback).verbose
  1896. )
  1897. ) {
  1898. // @ts-ignore
  1899. historyApiFallback.logger = this.logger.log.bind(
  1900. this.logger,
  1901. "[connect-history-api-fallback]",
  1902. );
  1903. }
  1904. // Fall back to /index.html if nothing else matches.
  1905. middlewares.push({
  1906. name: "connect-history-api-fallback",
  1907. middleware: connectHistoryApiFallback(
  1908. /** @type {ConnectHistoryApiFallbackOptions} */
  1909. (historyApiFallback),
  1910. ),
  1911. });
  1912. // include our middleware to ensure
  1913. // it is able to handle '/index.html' request after redirect
  1914. middlewares.push({
  1915. name: "webpack-dev-middleware",
  1916. middleware:
  1917. /** @type {import("webpack-dev-middleware").Middleware<Request, Response>}*/
  1918. (this.middleware),
  1919. });
  1920. if (/** @type {NormalizedStatic[]} */ (this.options.static).length > 0) {
  1921. /** @type {NormalizedStatic[]} */
  1922. (this.options.static).forEach((staticOption) => {
  1923. staticOption.publicPath.forEach((publicPath) => {
  1924. middlewares.push({
  1925. name: "express-static",
  1926. path: publicPath,
  1927. middleware: getExpress().static(
  1928. staticOption.directory,
  1929. staticOption.staticOptions,
  1930. ),
  1931. });
  1932. });
  1933. });
  1934. }
  1935. }
  1936. if (/** @type {NormalizedStatic[]} */ (this.options.static).length > 0) {
  1937. const serveIndex = require("serve-index");
  1938. /** @type {NormalizedStatic[]} */
  1939. (this.options.static).forEach((staticOption) => {
  1940. staticOption.publicPath.forEach((publicPath) => {
  1941. if (staticOption.serveIndex) {
  1942. middlewares.push({
  1943. name: "serve-index",
  1944. path: publicPath,
  1945. /**
  1946. * @param {Request} req
  1947. * @param {Response} res
  1948. * @param {NextFunction} next
  1949. * @returns {void}
  1950. */
  1951. middleware: (req, res, next) => {
  1952. // serve-index doesn't fallthrough non-get/head request to next middleware
  1953. if (req.method !== "GET" && req.method !== "HEAD") {
  1954. return next();
  1955. }
  1956. serveIndex(
  1957. staticOption.directory,
  1958. /** @type {ServeIndexOptions} */
  1959. (staticOption.serveIndex),
  1960. )(req, res, next);
  1961. },
  1962. });
  1963. }
  1964. });
  1965. });
  1966. }
  1967. // Register this middleware always as the last one so that it's only used as a
  1968. // fallback when no other middleware responses.
  1969. middlewares.push({
  1970. name: "options-middleware",
  1971. path: "*",
  1972. /**
  1973. * @param {Request} req
  1974. * @param {Response} res
  1975. * @param {NextFunction} next
  1976. * @returns {void}
  1977. */
  1978. middleware: (req, res, next) => {
  1979. if (req.method === "OPTIONS") {
  1980. res.statusCode = 204;
  1981. res.setHeader("Content-Length", "0");
  1982. res.end();
  1983. return;
  1984. }
  1985. next();
  1986. },
  1987. });
  1988. if (typeof this.options.setupMiddlewares === "function") {
  1989. middlewares = this.options.setupMiddlewares(middlewares, this);
  1990. }
  1991. middlewares.forEach((middleware) => {
  1992. if (typeof middleware === "function") {
  1993. /** @type {import("express").Application} */
  1994. (this.app).use(middleware);
  1995. } else if (typeof middleware.path !== "undefined") {
  1996. /** @type {import("express").Application} */
  1997. (this.app).use(middleware.path, middleware.middleware);
  1998. } else {
  1999. /** @type {import("express").Application} */
  2000. (this.app).use(middleware.middleware);
  2001. }
  2002. });
  2003. }
  2004. /**
  2005. * @private
  2006. * @returns {void}
  2007. */
  2008. createServer() {
  2009. const { type, options } = /** @type {ServerConfiguration} */ (
  2010. this.options.server
  2011. );
  2012. /** @type {import("http").Server | undefined | null} */
  2013. // eslint-disable-next-line import/no-dynamic-require
  2014. this.server = require(/** @type {string} */ (type)).createServer(
  2015. options,
  2016. this.app,
  2017. );
  2018. /** @type {import("http").Server} */
  2019. (this.server).on(
  2020. "connection",
  2021. /**
  2022. * @param {Socket} socket
  2023. */
  2024. (socket) => {
  2025. // Add socket to list
  2026. this.sockets.push(socket);
  2027. socket.once("close", () => {
  2028. // Remove socket from list
  2029. this.sockets.splice(this.sockets.indexOf(socket), 1);
  2030. });
  2031. },
  2032. );
  2033. /** @type {import("http").Server} */
  2034. (this.server).on(
  2035. "error",
  2036. /**
  2037. * @param {Error} error
  2038. */
  2039. (error) => {
  2040. throw error;
  2041. },
  2042. );
  2043. }
  2044. /**
  2045. * @private
  2046. * @returns {void}
  2047. */
  2048. createWebSocketServer() {
  2049. /** @type {WebSocketServerImplementation | undefined | null} */
  2050. this.webSocketServer = new /** @type {any} */ (this.getServerTransport())(
  2051. this,
  2052. );
  2053. /** @type {WebSocketServerImplementation} */
  2054. (this.webSocketServer).implementation.on(
  2055. "connection",
  2056. /**
  2057. * @param {ClientConnection} client
  2058. * @param {IncomingMessage} request
  2059. */
  2060. (client, request) => {
  2061. /** @type {{ [key: string]: string | undefined } | undefined} */
  2062. const headers =
  2063. // eslint-disable-next-line no-nested-ternary
  2064. typeof request !== "undefined"
  2065. ? /** @type {{ [key: string]: string | undefined }} */
  2066. (request.headers)
  2067. : typeof (
  2068. /** @type {import("sockjs").Connection} */ (client).headers
  2069. ) !== "undefined"
  2070. ? /** @type {import("sockjs").Connection} */ (client).headers
  2071. : // eslint-disable-next-line no-undefined
  2072. undefined;
  2073. if (!headers) {
  2074. this.logger.warn(
  2075. 'webSocketServer implementation must pass headers for the "connection" event',
  2076. );
  2077. }
  2078. if (
  2079. !headers ||
  2080. !this.checkHeader(headers, "host") ||
  2081. !this.checkHeader(headers, "origin")
  2082. ) {
  2083. this.sendMessage([client], "error", "Invalid Host/Origin header");
  2084. // With https enabled, the sendMessage above is encrypted asynchronously so not yet sent
  2085. // Terminate would prevent it sending, so use close to allow it to be sent
  2086. client.close();
  2087. return;
  2088. }
  2089. if (this.options.hot === true || this.options.hot === "only") {
  2090. this.sendMessage([client], "hot");
  2091. }
  2092. if (this.options.liveReload) {
  2093. this.sendMessage([client], "liveReload");
  2094. }
  2095. if (
  2096. this.options.client &&
  2097. /** @type {ClientConfiguration} */
  2098. (this.options.client).progress
  2099. ) {
  2100. this.sendMessage(
  2101. [client],
  2102. "progress",
  2103. /** @type {ClientConfiguration} */
  2104. (this.options.client).progress,
  2105. );
  2106. }
  2107. if (
  2108. this.options.client &&
  2109. /** @type {ClientConfiguration} */ (this.options.client).reconnect
  2110. ) {
  2111. this.sendMessage(
  2112. [client],
  2113. "reconnect",
  2114. /** @type {ClientConfiguration} */
  2115. (this.options.client).reconnect,
  2116. );
  2117. }
  2118. if (
  2119. this.options.client &&
  2120. /** @type {ClientConfiguration} */
  2121. (this.options.client).overlay
  2122. ) {
  2123. const overlayConfig = /** @type {ClientConfiguration} */ (
  2124. this.options.client
  2125. ).overlay;
  2126. this.sendMessage(
  2127. [client],
  2128. "overlay",
  2129. typeof overlayConfig === "object"
  2130. ? {
  2131. ...overlayConfig,
  2132. errors:
  2133. overlayConfig.errors &&
  2134. encodeOverlaySettings(overlayConfig.errors),
  2135. warnings:
  2136. overlayConfig.warnings &&
  2137. encodeOverlaySettings(overlayConfig.warnings),
  2138. runtimeErrors:
  2139. overlayConfig.runtimeErrors &&
  2140. encodeOverlaySettings(overlayConfig.runtimeErrors),
  2141. }
  2142. : overlayConfig,
  2143. );
  2144. }
  2145. if (!this.stats) {
  2146. return;
  2147. }
  2148. this.sendStats([client], this.getStats(this.stats), true);
  2149. },
  2150. );
  2151. }
  2152. /**
  2153. * @private
  2154. * @param {string} defaultOpenTarget
  2155. * @returns {Promise<void>}
  2156. */
  2157. async openBrowser(defaultOpenTarget) {
  2158. const open = (await import("open")).default;
  2159. Promise.all(
  2160. /** @type {NormalizedOpen[]} */
  2161. (this.options.open).map((item) => {
  2162. /**
  2163. * @type {string}
  2164. */
  2165. let openTarget;
  2166. if (item.target === "<url>") {
  2167. openTarget = defaultOpenTarget;
  2168. } else {
  2169. openTarget = Server.isAbsoluteURL(item.target)
  2170. ? item.target
  2171. : new URL(item.target, defaultOpenTarget).toString();
  2172. }
  2173. return open(openTarget, item.options).catch(() => {
  2174. this.logger.warn(
  2175. `Unable to open "${openTarget}" page${
  2176. item.options.app
  2177. ? ` in "${
  2178. /** @type {import("open").App} */
  2179. (item.options.app).name
  2180. }" app${
  2181. /** @type {import("open").App} */
  2182. (item.options.app).arguments
  2183. ? ` with "${
  2184. /** @type {import("open").App} */
  2185. (item.options.app).arguments.join(" ")
  2186. }" arguments`
  2187. : ""
  2188. }`
  2189. : ""
  2190. }. If you are running in a headless environment, please do not use the "open" option or related flags like "--open", "--open-target", and "--open-app-name".`,
  2191. );
  2192. });
  2193. }),
  2194. );
  2195. }
  2196. /**
  2197. * @private
  2198. * @returns {void}
  2199. */
  2200. runBonjour() {
  2201. const { Bonjour } = require("bonjour-service");
  2202. /**
  2203. * @private
  2204. * @type {Bonjour | undefined}
  2205. */
  2206. this.bonjour = new Bonjour();
  2207. this.bonjour.publish({
  2208. // @ts-expect-error
  2209. name: `Webpack Dev Server ${os.hostname()}:${this.options.port}`,
  2210. // @ts-expect-error
  2211. port: /** @type {number} */ (this.options.port),
  2212. // @ts-expect-error
  2213. type:
  2214. /** @type {ServerConfiguration} */
  2215. (this.options.server).type === "http" ? "http" : "https",
  2216. subtypes: ["webpack"],
  2217. .../** @type {BonjourOptions} */ (this.options.bonjour),
  2218. });
  2219. }
  2220. /**
  2221. * @private
  2222. * @returns {void}
  2223. */
  2224. stopBonjour(callback = () => {}) {
  2225. /** @type {Bonjour} */
  2226. (this.bonjour).unpublishAll(() => {
  2227. /** @type {Bonjour} */
  2228. (this.bonjour).destroy();
  2229. if (callback) {
  2230. callback();
  2231. }
  2232. });
  2233. }
  2234. /**
  2235. * @private
  2236. * @returns {Promise<void>}
  2237. */
  2238. async logStatus() {
  2239. const { isColorSupported, cyan, red } = require("colorette");
  2240. /**
  2241. * @param {Compiler["options"]} compilerOptions
  2242. * @returns {boolean}
  2243. */
  2244. const getColorsOption = (compilerOptions) => {
  2245. /**
  2246. * @type {boolean}
  2247. */
  2248. let colorsEnabled;
  2249. if (
  2250. compilerOptions.stats &&
  2251. typeof (/** @type {StatsOptions} */ (compilerOptions.stats).colors) !==
  2252. "undefined"
  2253. ) {
  2254. colorsEnabled =
  2255. /** @type {boolean} */
  2256. (/** @type {StatsOptions} */ (compilerOptions.stats).colors);
  2257. } else {
  2258. colorsEnabled = isColorSupported;
  2259. }
  2260. return colorsEnabled;
  2261. };
  2262. const colors = {
  2263. /**
  2264. * @param {boolean} useColor
  2265. * @param {string} msg
  2266. * @returns {string}
  2267. */
  2268. info(useColor, msg) {
  2269. if (useColor) {
  2270. return cyan(msg);
  2271. }
  2272. return msg;
  2273. },
  2274. /**
  2275. * @param {boolean} useColor
  2276. * @param {string} msg
  2277. * @returns {string}
  2278. */
  2279. error(useColor, msg) {
  2280. if (useColor) {
  2281. return red(msg);
  2282. }
  2283. return msg;
  2284. },
  2285. };
  2286. const useColor = getColorsOption(this.getCompilerOptions());
  2287. if (this.options.ipc) {
  2288. this.logger.info(
  2289. `Project is running at: "${
  2290. /** @type {import("http").Server} */
  2291. (this.server).address()
  2292. }"`,
  2293. );
  2294. } else {
  2295. const protocol =
  2296. /** @type {ServerConfiguration} */
  2297. (this.options.server).type === "http" ? "http" : "https";
  2298. const { address, port } =
  2299. /** @type {import("net").AddressInfo} */
  2300. (
  2301. /** @type {import("http").Server} */
  2302. (this.server).address()
  2303. );
  2304. /**
  2305. * @param {string} newHostname
  2306. * @returns {string}
  2307. */
  2308. const prettyPrintURL = (newHostname) =>
  2309. url.format({ protocol, hostname: newHostname, port, pathname: "/" });
  2310. let server;
  2311. let localhost;
  2312. let loopbackIPv4;
  2313. let loopbackIPv6;
  2314. let networkUrlIPv4;
  2315. let networkUrlIPv6;
  2316. if (this.options.host) {
  2317. if (this.options.host === "localhost") {
  2318. localhost = prettyPrintURL("localhost");
  2319. } else {
  2320. let isIP;
  2321. try {
  2322. isIP = ipaddr.parse(this.options.host);
  2323. } catch (error) {
  2324. // Ignore
  2325. }
  2326. if (!isIP) {
  2327. server = prettyPrintURL(this.options.host);
  2328. }
  2329. }
  2330. }
  2331. const parsedIP = ipaddr.parse(address);
  2332. if (parsedIP.range() === "unspecified") {
  2333. localhost = prettyPrintURL("localhost");
  2334. const networkIPv4 = await Server.internalIP("v4");
  2335. if (networkIPv4) {
  2336. networkUrlIPv4 = prettyPrintURL(networkIPv4);
  2337. }
  2338. const networkIPv6 = await Server.internalIP("v6");
  2339. if (networkIPv6) {
  2340. networkUrlIPv6 = prettyPrintURL(networkIPv6);
  2341. }
  2342. } else if (parsedIP.range() === "loopback") {
  2343. if (parsedIP.kind() === "ipv4") {
  2344. loopbackIPv4 = prettyPrintURL(parsedIP.toString());
  2345. } else if (parsedIP.kind() === "ipv6") {
  2346. loopbackIPv6 = prettyPrintURL(parsedIP.toString());
  2347. }
  2348. } else {
  2349. networkUrlIPv4 =
  2350. parsedIP.kind() === "ipv6" &&
  2351. /** @type {IPv6} */
  2352. (parsedIP).isIPv4MappedAddress()
  2353. ? prettyPrintURL(
  2354. /** @type {IPv6} */
  2355. (parsedIP).toIPv4Address().toString(),
  2356. )
  2357. : prettyPrintURL(address);
  2358. if (parsedIP.kind() === "ipv6") {
  2359. networkUrlIPv6 = prettyPrintURL(address);
  2360. }
  2361. }
  2362. this.logger.info("Project is running at:");
  2363. if (server) {
  2364. this.logger.info(`Server: ${colors.info(useColor, server)}`);
  2365. }
  2366. if (localhost || loopbackIPv4 || loopbackIPv6) {
  2367. const loopbacks = [];
  2368. if (localhost) {
  2369. loopbacks.push([colors.info(useColor, localhost)]);
  2370. }
  2371. if (loopbackIPv4) {
  2372. loopbacks.push([colors.info(useColor, loopbackIPv4)]);
  2373. }
  2374. if (loopbackIPv6) {
  2375. loopbacks.push([colors.info(useColor, loopbackIPv6)]);
  2376. }
  2377. this.logger.info(`Loopback: ${loopbacks.join(", ")}`);
  2378. }
  2379. if (networkUrlIPv4) {
  2380. this.logger.info(
  2381. `On Your Network (IPv4): ${colors.info(useColor, networkUrlIPv4)}`,
  2382. );
  2383. }
  2384. if (networkUrlIPv6) {
  2385. this.logger.info(
  2386. `On Your Network (IPv6): ${colors.info(useColor, networkUrlIPv6)}`,
  2387. );
  2388. }
  2389. if (/** @type {NormalizedOpen[]} */ (this.options.open).length > 0) {
  2390. const openTarget = prettyPrintURL(
  2391. !this.options.host ||
  2392. this.options.host === "0.0.0.0" ||
  2393. this.options.host === "::"
  2394. ? "localhost"
  2395. : this.options.host,
  2396. );
  2397. await this.openBrowser(openTarget);
  2398. }
  2399. }
  2400. if (/** @type {NormalizedStatic[]} */ (this.options.static).length > 0) {
  2401. this.logger.info(
  2402. `Content not from webpack is served from '${colors.info(
  2403. useColor,
  2404. /** @type {NormalizedStatic[]} */
  2405. (this.options.static)
  2406. .map((staticOption) => staticOption.directory)
  2407. .join(", "),
  2408. )}' directory`,
  2409. );
  2410. }
  2411. if (this.options.historyApiFallback) {
  2412. this.logger.info(
  2413. `404s will fallback to '${colors.info(
  2414. useColor,
  2415. /** @type {ConnectHistoryApiFallbackOptions} */ (
  2416. this.options.historyApiFallback
  2417. ).index || "/index.html",
  2418. )}'`,
  2419. );
  2420. }
  2421. if (this.options.bonjour) {
  2422. const bonjourProtocol =
  2423. /** @type {BonjourOptions} */
  2424. (this.options.bonjour).type ||
  2425. /** @type {ServerConfiguration} */
  2426. (this.options.server).type === "http"
  2427. ? "http"
  2428. : "https";
  2429. this.logger.info(
  2430. `Broadcasting "${bonjourProtocol}" with subtype of "webpack" via ZeroConf DNS (Bonjour)`,
  2431. );
  2432. }
  2433. }
  2434. /**
  2435. * @private
  2436. * @param {Request} req
  2437. * @param {Response} res
  2438. * @param {NextFunction} next
  2439. */
  2440. setHeaders(req, res, next) {
  2441. let { headers } = this.options;
  2442. if (headers) {
  2443. if (typeof headers === "function") {
  2444. headers = headers(
  2445. req,
  2446. res,
  2447. /** @type {import("webpack-dev-middleware").API<IncomingMessage, ServerResponse>}*/
  2448. (this.middleware).context,
  2449. );
  2450. }
  2451. /**
  2452. * @type {{key: string, value: string}[]}
  2453. */
  2454. const allHeaders = [];
  2455. if (!Array.isArray(headers)) {
  2456. // eslint-disable-next-line guard-for-in
  2457. for (const name in headers) {
  2458. // @ts-ignore
  2459. allHeaders.push({ key: name, value: headers[name] });
  2460. }
  2461. headers = allHeaders;
  2462. }
  2463. headers.forEach(
  2464. /**
  2465. * @param {{key: string, value: any}} header
  2466. */
  2467. (header) => {
  2468. res.setHeader(header.key, header.value);
  2469. },
  2470. );
  2471. }
  2472. next();
  2473. }
  2474. /**
  2475. * @private
  2476. * @param {{ [key: string]: string | undefined }} headers
  2477. * @param {string} headerToCheck
  2478. * @returns {boolean}
  2479. */
  2480. checkHeader(headers, headerToCheck) {
  2481. // allow user to opt out of this security check, at their own risk
  2482. // by explicitly enabling allowedHosts
  2483. if (this.options.allowedHosts === "all") {
  2484. return true;
  2485. }
  2486. // get the Host header and extract hostname
  2487. // we don't care about port not matching
  2488. const hostHeader = headers[headerToCheck];
  2489. if (!hostHeader) {
  2490. return false;
  2491. }
  2492. if (/^(file|.+-extension):/i.test(hostHeader)) {
  2493. return true;
  2494. }
  2495. // use the node url-parser to retrieve the hostname from the host-header.
  2496. const hostname = url.parse(
  2497. // if hostHeader doesn't have scheme, add // for parsing.
  2498. /^(.+:)?\/\//.test(hostHeader) ? hostHeader : `//${hostHeader}`,
  2499. false,
  2500. true,
  2501. ).hostname;
  2502. // always allow requests with explicit IPv4 or IPv6-address.
  2503. // A note on IPv6 addresses:
  2504. // hostHeader will always contain the brackets denoting
  2505. // an IPv6-address in URLs,
  2506. // these are removed from the hostname in url.parse(),
  2507. // so we have the pure IPv6-address in hostname.
  2508. // For convenience, always allow localhost (hostname === 'localhost')
  2509. // and its subdomains (hostname.endsWith(".localhost")).
  2510. // allow hostname of listening address (hostname === this.options.host)
  2511. const isValidHostname =
  2512. (hostname !== null && ipaddr.IPv4.isValid(hostname)) ||
  2513. (hostname !== null && ipaddr.IPv6.isValid(hostname)) ||
  2514. hostname === "localhost" ||
  2515. (hostname !== null && hostname.endsWith(".localhost")) ||
  2516. hostname === this.options.host;
  2517. if (isValidHostname) {
  2518. return true;
  2519. }
  2520. const { allowedHosts } = this.options;
  2521. // always allow localhost host, for convenience
  2522. // allow if hostname is in allowedHosts
  2523. if (Array.isArray(allowedHosts) && allowedHosts.length > 0) {
  2524. for (let hostIdx = 0; hostIdx < allowedHosts.length; hostIdx++) {
  2525. /** @type {string} */
  2526. const allowedHost = allowedHosts[hostIdx];
  2527. if (allowedHost === hostname) {
  2528. return true;
  2529. }
  2530. // support "." as a subdomain wildcard
  2531. // e.g. ".example.com" will allow "example.com", "www.example.com", "subdomain.example.com", etc
  2532. if (allowedHost[0] === ".") {
  2533. // "example.com" (hostname === allowedHost.substring(1))
  2534. // "*.example.com" (hostname.endsWith(allowedHost))
  2535. if (
  2536. hostname === allowedHost.substring(1) ||
  2537. /** @type {string} */ (hostname).endsWith(allowedHost)
  2538. ) {
  2539. return true;
  2540. }
  2541. }
  2542. }
  2543. }
  2544. // Also allow if `client.webSocketURL.hostname` provided
  2545. if (
  2546. this.options.client &&
  2547. typeof (
  2548. /** @type {ClientConfiguration} */ (this.options.client).webSocketURL
  2549. ) !== "undefined"
  2550. ) {
  2551. return (
  2552. /** @type {WebSocketURL} */
  2553. (/** @type {ClientConfiguration} */ (this.options.client).webSocketURL)
  2554. .hostname === hostname
  2555. );
  2556. }
  2557. // disallow
  2558. return false;
  2559. }
  2560. /**
  2561. * @param {ClientConnection[]} clients
  2562. * @param {string} type
  2563. * @param {any} [data]
  2564. * @param {any} [params]
  2565. */
  2566. // eslint-disable-next-line class-methods-use-this
  2567. sendMessage(clients, type, data, params) {
  2568. for (const client of clients) {
  2569. // `sockjs` uses `1` to indicate client is ready to accept data
  2570. // `ws` uses `WebSocket.OPEN`, but it is mean `1` too
  2571. if (client.readyState === 1) {
  2572. client.send(JSON.stringify({ type, data, params }));
  2573. }
  2574. }
  2575. }
  2576. // Send stats to a socket or multiple sockets
  2577. /**
  2578. * @private
  2579. * @param {ClientConnection[]} clients
  2580. * @param {StatsCompilation} stats
  2581. * @param {boolean} [force]
  2582. */
  2583. sendStats(clients, stats, force) {
  2584. const shouldEmit =
  2585. !force &&
  2586. stats &&
  2587. (!stats.errors || stats.errors.length === 0) &&
  2588. (!stats.warnings || stats.warnings.length === 0) &&
  2589. this.currentHash === stats.hash;
  2590. if (shouldEmit) {
  2591. this.sendMessage(clients, "still-ok");
  2592. return;
  2593. }
  2594. this.currentHash = stats.hash;
  2595. this.sendMessage(clients, "hash", stats.hash);
  2596. if (
  2597. /** @type {NonNullable<StatsCompilation["errors"]>} */
  2598. (stats.errors).length > 0 ||
  2599. /** @type {NonNullable<StatsCompilation["warnings"]>} */
  2600. (stats.warnings).length > 0
  2601. ) {
  2602. const hasErrors =
  2603. /** @type {NonNullable<StatsCompilation["errors"]>} */
  2604. (stats.errors).length > 0;
  2605. if (
  2606. /** @type {NonNullable<StatsCompilation["warnings"]>} */
  2607. (stats.warnings).length > 0
  2608. ) {
  2609. let params;
  2610. if (hasErrors) {
  2611. params = { preventReloading: true };
  2612. }
  2613. this.sendMessage(clients, "warnings", stats.warnings, params);
  2614. }
  2615. if (
  2616. /** @type {NonNullable<StatsCompilation["errors"]>} */ (stats.errors)
  2617. .length > 0
  2618. ) {
  2619. this.sendMessage(clients, "errors", stats.errors);
  2620. }
  2621. } else {
  2622. this.sendMessage(clients, "ok");
  2623. }
  2624. }
  2625. /**
  2626. * @param {string | string[]} watchPath
  2627. * @param {WatchOptions} [watchOptions]
  2628. */
  2629. watchFiles(watchPath, watchOptions) {
  2630. const chokidar = require("chokidar");
  2631. const watcher = chokidar.watch(watchPath, watchOptions);
  2632. // disabling refreshing on changing the content
  2633. if (this.options.liveReload) {
  2634. watcher.on("change", (item) => {
  2635. if (this.webSocketServer) {
  2636. this.sendMessage(
  2637. this.webSocketServer.clients,
  2638. "static-changed",
  2639. item,
  2640. );
  2641. }
  2642. });
  2643. }
  2644. this.staticWatchers.push(watcher);
  2645. }
  2646. /**
  2647. * @param {import("webpack-dev-middleware").Callback} [callback]
  2648. */
  2649. invalidate(callback = () => {}) {
  2650. if (this.middleware) {
  2651. this.middleware.invalidate(callback);
  2652. }
  2653. }
  2654. /**
  2655. * @returns {Promise<void>}
  2656. */
  2657. async start() {
  2658. await this.normalizeOptions();
  2659. if (this.options.ipc) {
  2660. await /** @type {Promise<void>} */ (
  2661. new Promise((resolve, reject) => {
  2662. const net = require("net");
  2663. const socket = new net.Socket();
  2664. socket.on(
  2665. "error",
  2666. /**
  2667. * @param {Error & { code?: string }} error
  2668. */
  2669. (error) => {
  2670. if (error.code === "ECONNREFUSED") {
  2671. // No other server listening on this socket, so it can be safely removed
  2672. fs.unlinkSync(/** @type {string} */ (this.options.ipc));
  2673. resolve();
  2674. return;
  2675. } else if (error.code === "ENOENT") {
  2676. resolve();
  2677. return;
  2678. }
  2679. reject(error);
  2680. },
  2681. );
  2682. socket.connect(
  2683. { path: /** @type {string} */ (this.options.ipc) },
  2684. () => {
  2685. throw new Error(`IPC "${this.options.ipc}" is already used`);
  2686. },
  2687. );
  2688. })
  2689. );
  2690. } else {
  2691. this.options.host = await Server.getHostname(
  2692. /** @type {Host} */ (this.options.host),
  2693. );
  2694. this.options.port = await Server.getFreePort(
  2695. /** @type {Port} */ (this.options.port),
  2696. this.options.host,
  2697. );
  2698. }
  2699. await this.initialize();
  2700. const listenOptions = this.options.ipc
  2701. ? { path: this.options.ipc }
  2702. : { host: this.options.host, port: this.options.port };
  2703. await /** @type {Promise<void>} */ (
  2704. new Promise((resolve) => {
  2705. /** @type {import("http").Server} */
  2706. (this.server).listen(listenOptions, () => {
  2707. resolve();
  2708. });
  2709. })
  2710. );
  2711. if (this.options.ipc) {
  2712. // chmod 666 (rw rw rw)
  2713. const READ_WRITE = 438;
  2714. await fs.promises.chmod(
  2715. /** @type {string} */ (this.options.ipc),
  2716. READ_WRITE,
  2717. );
  2718. }
  2719. if (this.options.webSocketServer) {
  2720. this.createWebSocketServer();
  2721. }
  2722. if (this.options.bonjour) {
  2723. this.runBonjour();
  2724. }
  2725. await this.logStatus();
  2726. if (typeof this.options.onListening === "function") {
  2727. this.options.onListening(this);
  2728. }
  2729. }
  2730. /**
  2731. * @param {(err?: Error) => void} [callback]
  2732. */
  2733. startCallback(callback = () => {}) {
  2734. this.start()
  2735. .then(() => callback(), callback)
  2736. .catch(callback);
  2737. }
  2738. /**
  2739. * @returns {Promise<void>}
  2740. */
  2741. async stop() {
  2742. if (this.bonjour) {
  2743. await /** @type {Promise<void>} */ (
  2744. new Promise((resolve) => {
  2745. this.stopBonjour(() => {
  2746. resolve();
  2747. });
  2748. })
  2749. );
  2750. }
  2751. this.webSocketProxies = [];
  2752. await Promise.all(this.staticWatchers.map((watcher) => watcher.close()));
  2753. this.staticWatchers = [];
  2754. if (this.webSocketServer) {
  2755. await /** @type {Promise<void>} */ (
  2756. new Promise((resolve) => {
  2757. /** @type {WebSocketServerImplementation} */
  2758. (this.webSocketServer).implementation.close(() => {
  2759. this.webSocketServer = null;
  2760. resolve();
  2761. });
  2762. for (const client of /** @type {WebSocketServerImplementation} */ (
  2763. this.webSocketServer
  2764. ).clients) {
  2765. client.terminate();
  2766. }
  2767. /** @type {WebSocketServerImplementation} */
  2768. (this.webSocketServer).clients = [];
  2769. })
  2770. );
  2771. }
  2772. if (this.server) {
  2773. await /** @type {Promise<void>} */ (
  2774. new Promise((resolve) => {
  2775. /** @type {import("http").Server} */
  2776. (this.server).close(() => {
  2777. this.server = null;
  2778. resolve();
  2779. });
  2780. for (const socket of this.sockets) {
  2781. socket.destroy();
  2782. }
  2783. this.sockets = [];
  2784. })
  2785. );
  2786. if (this.middleware) {
  2787. await /** @type {Promise<void>} */ (
  2788. new Promise((resolve, reject) => {
  2789. /** @type {import("webpack-dev-middleware").API<Request, Response>}*/
  2790. (this.middleware).close((error) => {
  2791. if (error) {
  2792. reject(error);
  2793. return;
  2794. }
  2795. resolve();
  2796. });
  2797. })
  2798. );
  2799. this.middleware = null;
  2800. }
  2801. }
  2802. // We add listeners to signals when creating a new Server instance
  2803. // So ensure they are removed to prevent EventEmitter memory leak warnings
  2804. for (const item of this.listeners) {
  2805. process.removeListener(item.name, item.listener);
  2806. }
  2807. }
  2808. /**
  2809. * @param {(err?: Error) => void} [callback]
  2810. */
  2811. stopCallback(callback = () => {}) {
  2812. this.stop()
  2813. .then(() => callback(), callback)
  2814. .catch(callback);
  2815. }
  2816. }
  2817. module.exports = Server;