Skip to content

Commit 3ef2985

Browse files
refactor: merge more utils
1 parent 5e0c590 commit 3ef2985

8 files changed

Lines changed: 286 additions & 727 deletions

src/index.js

Lines changed: 286 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1+
const fs = require("node:fs");
2+
const path = require("node:path");
3+
const memfs = require("memfs");
14
const mime = require("mime-types");
25

36
const middleware = require("./middleware");
47
const getFilenameFromUrl = require("./utils/getFilenameFromUrl");
58
const ready = require("./utils/ready");
6-
const setupHooks = require("./utils/setupHooks");
7-
const setupOutputFileSystem = require("./utils/setupOutputFileSystem");
8-
const setupWriteToDisk = require("./utils/setupWriteToDisk");
99

1010
const noop = () => {};
1111

@@ -197,9 +197,11 @@ const noop = () => {};
197197
const 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) {

src/middleware.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
const path = require("node:path");
22

33
const mime = require("mime-types");
4-
54
const onFinishedStream = require("on-finished");
65

76
const { escapeHtml, etag, memorize, parseTokenList } = require("./utils");

0 commit comments

Comments
 (0)