Server.js 110 KB

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