Server.js 103 KB

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