@@ -329,26 +329,31 @@ const DEFAULT_ALLOWED_PROTOCOLS = /^(file|.+-extension):/i;
329329 * @property {typeof useFn } use
330330 */
331331
332+ const pluginName = "webpack-dev-server" ;
333+
332334/**
333335 * @template {BasicApplication} [A=ExpressApplication]
334336 * @template {BasicServer} [S=HTTPServer]
335337 */
336338class Server {
337339 /**
338340 * @param {Configuration<A, S> } options options
339- * @param {Compiler | MultiCompiler } compiler compiler
341+ * @param {( Compiler | MultiCompiler)= } compiler compiler, omitted when the server is used as a plugin via `apply()`
340342 */
341343 constructor ( options , compiler ) {
342344 validate ( /** @type {Schema } */ ( schema ) , options , {
343345 name : "Dev Server" ,
344346 baseDataPath : "options" ,
345347 } ) ;
346348
347- this . compiler = compiler ;
348- /**
349- * @type {ReturnType<Compiler["getInfrastructureLogger"]> }
350- */
351- this . logger = this . compiler . getInfrastructureLogger ( "webpack-dev-server" ) ;
349+ if ( compiler ) {
350+ this . compiler = compiler ;
351+
352+ /**
353+ * @type {ReturnType<Compiler["getInfrastructureLogger"]> }
354+ */
355+ this . logger = this . compiler . getInfrastructureLogger ( pluginName ) ;
356+ }
352357 this . options = options ;
353358 /**
354359 * @type {FSWatcher[] }
@@ -375,6 +380,11 @@ class Server {
375380 */
376381
377382 this . currentHash = undefined ;
383+ /**
384+ * @private
385+ * @type {boolean }
386+ */
387+ this . isPlugin = false ;
378388 }
379389
380390 static get schema ( ) {
@@ -561,14 +571,14 @@ class Server {
561571 }
562572
563573 if ( ! dir ) {
564- return path . resolve ( cwd , " .cache/webpack-dev-server" ) ;
574+ return path . resolve ( cwd , ` .cache/${ pluginName } ` ) ;
565575 } else if ( process . versions . pnp === "1" ) {
566- return path . resolve ( dir , " .pnp/.cache/webpack-dev-server" ) ;
576+ return path . resolve ( dir , ` .pnp/.cache/${ pluginName } ` ) ;
567577 } else if ( process . versions . pnp === "3" ) {
568- return path . resolve ( dir , " .yarn/.cache/webpack-dev-server" ) ;
578+ return path . resolve ( dir , ` .yarn/.cache/${ pluginName } ` ) ;
569579 }
570580
571- return path . resolve ( dir , " node_modules/.cache/webpack-dev-server" ) ;
581+ return path . resolve ( dir , ` node_modules/.cache/${ pluginName } ` ) ;
572582 }
573583
574584 /**
@@ -583,7 +593,7 @@ class Server {
583593 // client. `target: false` is `null` everywhere, so it is excluded.
584594 return Boolean (
585595 platform . web ||
586- platform ?. universal ||
596+ /** @type { { universal?: boolean | null } } */ ( platform ) ?. universal ||
587597 ( compiler . options . target !== false &&
588598 platform . web === null &&
589599 platform . node === null ) ,
@@ -1263,7 +1273,7 @@ class Server {
12631273 if ( typeof options . ipc === "boolean" ) {
12641274 const isWindows = process . platform === "win32" ;
12651275 const pipePrefix = isWindows ? "\\\\.\\pipe\\" : os . tmpdir ( ) ;
1266- const pipeName = "webpack-dev-server .sock" ;
1276+ const pipeName = ` ${ pluginName } .sock` ;
12671277
12681278 options . ipc = path . join ( pipePrefix , pipeName ) ;
12691279 }
@@ -1362,7 +1372,12 @@ class Server {
13621372 }
13631373
13641374 if ( typeof options . setupExitSignals === "undefined" ) {
1365- options . setupExitSignals = true ;
1375+ // In plugin mode, the host (e.g. `webpack-cli`) usually owns process
1376+ // signal handling and calls `compiler.close()` on shutdown, which fires
1377+ // our `shutdown` hook. Adding our own SIGINT/SIGTERM listeners on top of
1378+ // that would race with the host's handler and call `compiler.close()`
1379+ // twice.
1380+ options . setupExitSignals = ! this . isPlugin ;
13661381 }
13671382
13681383 if ( typeof options . static === "undefined" ) {
@@ -1658,7 +1673,7 @@ class Server {
16581673 this . server . emit ( "progress-update" , { percent, msg, pluginName } ) ;
16591674 }
16601675 } ,
1661- ) . apply ( this . compiler ) ;
1676+ ) . apply ( /** @type { Compiler | MultiCompiler } */ ( this . compiler ) ) ;
16621677 }
16631678
16641679 /**
@@ -1777,7 +1792,7 @@ class Server {
17771792 needForceShutdown = true ;
17781793
17791794 this . stopCallback ( ( ) => {
1780- if ( typeof this . compiler . close === "function" ) {
1795+ if ( typeof this . compiler ? .close === "function" ) {
17811796 this . compiler . close ( ( ) => {
17821797 // eslint-disable-next-line n/no-process-exit
17831798 process . exit ( ) ;
@@ -1878,13 +1893,16 @@ class Server {
18781893 * @returns {void }
18791894 */
18801895 setupHooks ( ) {
1881- this . compiler . hooks . invalid . tap ( "webpack-dev-server" , ( ) => {
1896+ const compiler = /** @type {Compiler | MultiCompiler } */ ( this . compiler ) ;
1897+
1898+ compiler . hooks . invalid . tap ( pluginName , ( ) => {
18821899 if ( this . webSocketServer ) {
18831900 this . sendMessage ( this . webSocketServer . clients , "invalid" ) ;
18841901 }
18851902 } ) ;
1886- this . compiler . hooks . done . tap (
1887- "webpack-dev-server" ,
1903+
1904+ compiler . hooks . done . tap (
1905+ pluginName ,
18881906 /**
18891907 * @param {Stats | MultiStats } stats stats
18901908 */
@@ -1937,6 +1955,7 @@ class Server {
19371955 * @returns {Promise<void> }
19381956 */
19391957 async setupMiddlewares ( ) {
1958+ if ( this . compiler === undefined ) return ;
19401959 /**
19411960 * @type {Middleware[] }
19421961 */
@@ -2440,8 +2459,10 @@ class Server {
24402459 // middleware for serving webpack bundle
24412460 /** @type {import("webpack-dev-middleware").API<Request, Response> } */
24422461 this . middleware = webpackDevMiddleware (
2462+ // @ts -expect-error
24432463 this . compiler ,
24442464 this . options . devMiddleware ,
2465+ this . isPlugin ,
24452466 ) ;
24462467 }
24472468
@@ -3410,6 +3431,15 @@ class Server {
34103431 * @returns {Promise<void> }
34113432 */
34123433 async start ( ) {
3434+ await this . setup ( ) ;
3435+ await this . listen ( ) ;
3436+ }
3437+
3438+ /**
3439+ * @private
3440+ * @returns {Promise<void> }
3441+ */
3442+ async setup ( ) {
34133443 await this . normalizeOptions ( ) ;
34143444
34153445 if ( this . options . ipc ) {
@@ -3461,7 +3491,13 @@ class Server {
34613491 }
34623492
34633493 await this . initialize ( ) ;
3494+ }
34643495
3496+ /**
3497+ * @private
3498+ * @returns {Promise<void> }
3499+ */
3500+ async listen ( ) {
34653501 const listenOptions = this . options . ipc
34663502 ? { path : this . options . ipc }
34673503 : { host : this . options . host , port : this . options . port } ;
@@ -3613,6 +3649,88 @@ class Server {
36133649 . then ( ( ) => callback ( ) , callback )
36143650 . catch ( callback ) ;
36153651 }
3652+
3653+ /**
3654+ * @param {Compiler | MultiCompiler } compiler compiler
3655+ * @returns {void }
3656+ */
3657+ apply ( compiler ) {
3658+ this . compiler = compiler ;
3659+ this . isPlugin = true ;
3660+ this . logger = this . compiler . getInfrastructureLogger ( pluginName ) ;
3661+
3662+ /** @type {Promise<void> | undefined } */
3663+ let setupPromise ;
3664+ let inWatchMode = false ;
3665+ let listening = false ;
3666+ let stopped = false ;
3667+
3668+ // `setup()` boots webpack-dev-middleware, which replaces the compiler's
3669+ // `outputFileSystem` with an in-memory one. That swap has to happen before
3670+ // the first compilation writes its assets, otherwise the first build lands
3671+ // on the real disk — so it runs on `watchRun`, at the start of a watch run,
3672+ // before anything is emitted. Guarded so the async `setup()` runs at most
3673+ // once across rebuilds.
3674+ /**
3675+ * @returns {Promise<void> } promise
3676+ */
3677+ const ensureSetup = ( ) => {
3678+ if ( ! setupPromise ) {
3679+ setupPromise = this . setup ( ) ;
3680+ }
3681+ return setupPromise ;
3682+ } ;
3683+
3684+ // `watchRun` and `done` are tapped on the compiler directly — no iteration
3685+ // over `MultiCompiler.compilers`:
3686+ // - `watchRun` is a `MultiHook` on a `MultiCompiler`, so the tap is forwarded
3687+ // to every child and awaited. It stays `tapPromise` so a failing `setup()`
3688+ // rejects the user's `watch()` callback. It only fires in watch mode, which
3689+ // is how we know a one-shot `compiler.run()` build is not in play.
3690+ // - `done` on a `MultiCompiler` is the aggregate `SyncHook` that fires once
3691+ // after every child finishes, so the server starts exactly once. Being a
3692+ // `SyncHook`, it can only be `tap`ped; `listen()` runs detached.
3693+ const { hooks } = /** @type {Compiler } */ ( compiler ) ;
3694+
3695+ hooks . watchRun . tapPromise ( pluginName , ( ) => {
3696+ inWatchMode = true ;
3697+ return ensureSetup ( ) ;
3698+ } ) ;
3699+
3700+ hooks . done . tap ( pluginName , ( ) => {
3701+ // `done` also fires for a one-shot `compiler.run()` build, where no
3702+ // `watchRun` ran; staying passive lets that build finish and exit.
3703+ if ( listening || ! inWatchMode ) return ;
3704+ listening = true ;
3705+ ensureSetup ( )
3706+ . then ( ( ) => this . listen ( ) )
3707+ . catch ( ( error ) => {
3708+ this . logger . error ( error ) ;
3709+ } ) ;
3710+ } ) ;
3711+
3712+ /**
3713+ * @returns {Promise<void> } promise
3714+ */
3715+ const onShutdown = async ( ) => {
3716+ if ( stopped ) return ;
3717+ stopped = true ;
3718+ await this . stop ( ) ;
3719+ } ;
3720+
3721+ // Teardown is the one place a loop is unavoidable. A `MultiCompiler` has no
3722+ // `shutdown` hook, and its aggregate `watchClose` does NOT fire on
3723+ // `compiler.close()` (only on `watching.close()`), so the only signal that
3724+ // survives `compiler.close()` is each child's own `shutdown`. Tapping it
3725+ // with `tapPromise` also lets `compiler.close()` await the server actually
3726+ // stopping, so the port is released before the next start.
3727+ const childCompilers = /** @type {MultiCompiler } */ ( compiler )
3728+ . compilers || [ compiler ] ;
3729+
3730+ for ( const childCompiler of childCompilers ) {
3731+ childCompiler . hooks . shutdown . tapPromise ( pluginName , onShutdown ) ;
3732+ }
3733+ }
36163734}
36173735
36183736export default Server ;
0 commit comments