1+ const fs = require ( "node:fs" ) ;
2+ const path = require ( "node:path" ) ;
3+ const memfs = require ( "memfs" ) ;
14const mime = require ( "mime-types" ) ;
25
36const middleware = require ( "./middleware" ) ;
47const getFilenameFromUrl = require ( "./utils/getFilenameFromUrl" ) ;
58const ready = require ( "./utils/ready" ) ;
6- const setupHooks = require ( "./utils/setupHooks" ) ;
7- const setupOutputFileSystem = require ( "./utils/setupOutputFileSystem" ) ;
8- const setupWriteToDisk = require ( "./utils/setupWriteToDisk" ) ;
99
1010const noop = ( ) => { } ;
1111
@@ -197,9 +197,11 @@ const noop = () => {};
197197const internalValidate = ( compiler , options ) => {
198198 const schema = require ( "./options.json" ) ;
199199
200- const firstCompiler = Array . isArray ( compiler )
201- ? compiler [ 0 ]
202- : /** @type {Compiler } */ compiler ;
200+ const firstCompiler = /** @type {Compiler & { validate: EXPECTED_ANY } } */ (
201+ Array . isArray ( /** @type {MultiCompiler } */ ( compiler ) . compilers )
202+ ? /** @type {MultiCompiler } */ ( compiler ) . compilers [ 0 ]
203+ : /** @type {Compiler } */ compiler
204+ ) ;
203205
204206 if ( typeof firstCompiler . validate === "function" ) {
205207 firstCompiler . validate ( schema , options , {
@@ -218,6 +220,166 @@ const internalValidate = (compiler, options) => {
218220 } ) ;
219221} ;
220222
223+ /** @typedef {Configuration["stats"] } StatsOptions */
224+ /** @typedef {{ children: Configuration["stats"][] } } MultiStatsOptions */
225+ /** @typedef {Exclude<Configuration["stats"], boolean | string | undefined> } StatsObjectOptions */
226+
227+ /**
228+ * @template {IncomingMessage} Request
229+ * @template {ServerResponse} Response
230+ * @param {WithOptional<Context<Request, Response>, "watching" | "outputFileSystem"> } context context
231+ * @param {boolean= } isPlugin true when it is a plugin usage, otherwise false
232+ */
233+ function setupHooks ( context , isPlugin ) {
234+ /**
235+ * @returns {void }
236+ */
237+ function invalid ( ) {
238+ if ( context . state ) {
239+ context . logger . log ( "Compilation starting..." ) ;
240+ }
241+
242+ // We are now in invalid state
243+
244+ context . state = false ;
245+
246+ context . stats = undefined ;
247+ }
248+
249+ /**
250+ * @param {StatsOptions } statsOptions stats options
251+ * @returns {StatsObjectOptions } object stats options
252+ */
253+ function normalizeStatsOptions ( statsOptions ) {
254+ if ( typeof statsOptions === "undefined" ) {
255+ statsOptions = { preset : "normal" } ;
256+ } else if ( typeof statsOptions === "boolean" ) {
257+ statsOptions = statsOptions ? { preset : "normal" } : { preset : "none" } ;
258+ } else if ( typeof statsOptions === "string" ) {
259+ statsOptions = { preset : statsOptions } ;
260+ }
261+
262+ return statsOptions ;
263+ }
264+
265+ /**
266+ * @param {Stats | MultiStats } stats stats
267+ */
268+ function done ( stats ) {
269+ // We are now on valid state
270+
271+ context . state = true ;
272+ context . stats = stats ;
273+
274+ // Do the stuff in nextTick, because bundle may be invalidated if a change happened while compiling
275+ process . nextTick ( ( ) => {
276+ const { compiler, logger, options, state, callbacks } = context ;
277+
278+ // Check if still in valid state
279+ if ( ! state ) {
280+ return ;
281+ }
282+
283+ // For plugin support we should print nothing, because webpack/webpack-cli/webpack-dev-server will print them on using `stats.toString()`
284+ if ( ! isPlugin ) {
285+ logger . log ( "Compilation finished" ) ;
286+
287+ const isMultiCompilerMode = Boolean (
288+ /** @type {MultiCompiler } */
289+ ( compiler ) . compilers ,
290+ ) ;
291+
292+ /**
293+ * @type {StatsOptions | MultiStatsOptions | undefined }
294+ */
295+ let statsOptions ;
296+
297+ if ( typeof options . stats !== "undefined" ) {
298+ statsOptions = isMultiCompilerMode
299+ ? {
300+ children :
301+ /** @type {MultiCompiler } */
302+ ( compiler ) . compilers . map ( ( ) => options . stats ) ,
303+ }
304+ : options . stats ;
305+ } else {
306+ statsOptions = isMultiCompilerMode
307+ ? {
308+ children :
309+ /** @type {MultiCompiler } */
310+ ( compiler ) . compilers . map ( ( child ) => child . options . stats ) ,
311+ }
312+ : /** @type {Compiler } */ ( compiler ) . options . stats ;
313+ }
314+
315+ if ( isMultiCompilerMode ) {
316+ /** @type {MultiStatsOptions } */
317+ ( statsOptions ) . children =
318+ /** @type {MultiStatsOptions } */
319+ ( statsOptions ) . children . map (
320+ /**
321+ * @param {StatsOptions } childStatsOptions child stats options
322+ * @returns {StatsObjectOptions } object child stats options
323+ */
324+ ( childStatsOptions ) => {
325+ childStatsOptions = normalizeStatsOptions ( childStatsOptions ) ;
326+
327+ if ( typeof childStatsOptions . colors === "undefined" ) {
328+ const [ firstCompiler ] =
329+ /** @type {MultiCompiler } */
330+ ( compiler ) . compilers ;
331+
332+ childStatsOptions . colors =
333+ firstCompiler . webpack . cli . isColorSupported ( ) ;
334+ }
335+
336+ return childStatsOptions ;
337+ } ,
338+ ) ;
339+ } else {
340+ statsOptions = normalizeStatsOptions (
341+ /** @type {StatsOptions } */ ( statsOptions ) ,
342+ ) ;
343+
344+ if ( typeof statsOptions . colors === "undefined" ) {
345+ const { compiler } = /** @type {{ compiler: Compiler } } */ (
346+ context
347+ ) ;
348+ statsOptions . colors = compiler . webpack . cli . isColorSupported ( ) ;
349+ }
350+ }
351+
352+ const printedStats = stats . toString (
353+ /** @type {StatsObjectOptions } */
354+ ( statsOptions ) ,
355+ ) ;
356+
357+ // Avoid extra empty line when `stats: 'none'`
358+ if ( printedStats ) {
359+ // eslint-disable-next-line no-console
360+ console . log ( printedStats ) ;
361+ }
362+ }
363+
364+ context . callbacks = [ ] ;
365+
366+ // Execute callback that are delayed
367+ for ( const callback of callbacks ) {
368+ callback ( stats ) ;
369+ }
370+ } ) ;
371+ }
372+
373+ // eslint-disable-next-line prefer-destructuring
374+ const compiler =
375+ /** @type {Context<Request, Response> } */
376+ ( context ) . compiler ;
377+
378+ compiler . hooks . watchRun . tap ( "webpack-dev-middleware" , invalid ) ;
379+ compiler . hooks . invalid . tap ( "webpack-dev-middleware" , invalid ) ;
380+ compiler . hooks . done . tap ( "webpack-dev-middleware" , done ) ;
381+ }
382+
221383/**
222384 * @template {IncomingMessage} [RequestInternal=IncomingMessage]
223385 * @template {ServerResponse} [ResponseInternal=ServerResponse]
@@ -255,10 +417,126 @@ function wdm(compiler, options = {}, isPlugin = false) {
255417 setupHooks ( context , isPlugin ) ;
256418
257419 if ( typeof options . writeToDisk === "function" ) {
258- setupWriteToDisk ( context ) ;
420+ /**
421+ * @type {Compiler[] }
422+ */
423+ const compilers =
424+ /** @type {MultiCompiler } */
425+ ( context . compiler ) . compilers || [ context . compiler ] ;
426+
427+ for ( const compiler of compilers ) {
428+ if ( compiler . options . devServer === false ) {
429+ continue ;
430+ }
431+
432+ compiler . hooks . emit . tap ( "DevMiddleware" , ( ) => {
433+ // @ts -expect-error
434+ if ( compiler . hasWebpackDevMiddlewareAssetEmittedCallback ) {
435+ return ;
436+ }
437+
438+ compiler . hooks . assetEmitted . tapAsync (
439+ "DevMiddleware" ,
440+ ( file , info , callback ) => {
441+ const { targetPath, content } = info ;
442+ const { writeToDisk : filter } = context . options ;
443+ const allowWrite =
444+ filter && typeof filter === "function"
445+ ? filter ( targetPath )
446+ : true ;
447+
448+ if ( ! allowWrite ) {
449+ return callback ( ) ;
450+ }
451+
452+ const dir = path . dirname ( targetPath ) ;
453+ const name = compiler . options . name
454+ ? `Child "${ compiler . options . name } ": `
455+ : "" ;
456+
457+ return fs . mkdir ( dir , { recursive : true } , ( mkdirError ) => {
458+ if ( mkdirError ) {
459+ context . logger . error (
460+ `${ name } Unable to write "${ dir } " directory to disk:\n${ mkdirError } ` ,
461+ ) ;
462+
463+ return callback ( mkdirError ) ;
464+ }
465+
466+ return fs . writeFile ( targetPath , content , ( writeFileError ) => {
467+ if ( writeFileError ) {
468+ context . logger . error (
469+ `${ name } Unable to write "${ targetPath } " asset to disk:\n${ writeFileError } ` ,
470+ ) ;
471+
472+ return callback ( writeFileError ) ;
473+ }
474+
475+ context . logger . log (
476+ `${ name } Asset written to disk: "${ targetPath } "` ,
477+ ) ;
478+
479+ return callback ( ) ;
480+ } ) ;
481+ } ) ;
482+ } ,
483+ ) ;
484+
485+ // @ts -expect-error
486+ compiler . hasWebpackDevMiddlewareAssetEmittedCallback = true ;
487+ } ) ;
488+ }
489+ }
490+
491+ let outputFileSystem ;
492+
493+ if ( context . options . outputFileSystem ) {
494+ const { outputFileSystem : outputFileSystemFromOptions } = context . options ;
495+
496+ outputFileSystem = outputFileSystemFromOptions ;
497+ }
498+ // Don't use `memfs` when developer wants to write everything to a disk, because it doesn't make sense.
499+ else if ( context . options . writeToDisk !== true ) {
500+ outputFileSystem = memfs . createFsFromVolume ( new memfs . Volume ( ) ) ;
501+ } else {
502+ const isMultiCompiler =
503+ /** @type {MultiCompiler } */
504+ ( context . compiler ) . compilers ;
505+
506+ if ( isMultiCompiler ) {
507+ // Prefer compiler with `devServer` option or fallback on the first
508+ const compiler =
509+ /** @type {MultiCompiler } */
510+ ( context . compiler ) . compilers . find (
511+ ( item ) =>
512+ Object . hasOwn ( item . options , "devServer" ) &&
513+ item . options . devServer !== false ,
514+ ) ;
515+
516+ ( { outputFileSystem } =
517+ compiler ||
518+ /** @type {MultiCompiler } */
519+ ( context . compiler ) . compilers [ 0 ] ) ;
520+ } else {
521+ ( { outputFileSystem } = context . compiler ) ;
522+ }
523+ }
524+
525+ const compilers =
526+ /** @type {MultiCompiler } */
527+ ( context . compiler ) . compilers || [ context . compiler ] ;
528+
529+ for ( const compiler of compilers ) {
530+ if ( compiler . options . devServer === false ) {
531+ continue ;
532+ }
533+
534+ // @ts -expect-error
535+ compiler . outputFileSystem = outputFileSystem ;
259536 }
260537
261- setupOutputFileSystem ( context ) ;
538+ // @ts -expect-error
539+ context . outputFileSystem = outputFileSystem ;
262540
263541 // Start watching
264542 if ( ! isPlugin ) {
0 commit comments