| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582 | /*	MIT License http://www.opensource.org/licenses/mit-license.php	Author Tobias Koppers @sokra*/"use strict";const asyncLib = require("neo-async");const { SyncHook, MultiHook } = require("tapable");const ConcurrentCompilationError = require("./ConcurrentCompilationError");const MultiStats = require("./MultiStats");const MultiWatching = require("./MultiWatching");const ArrayQueue = require("./util/ArrayQueue");/** @template T @typedef {import("tapable").AsyncSeriesHook<T>} AsyncSeriesHook<T> *//** @template T @template R @typedef {import("tapable").SyncBailHook<T, R>} SyncBailHook<T, R> *//** @typedef {import("../declarations/WebpackOptions").WatchOptions} WatchOptions *//** @typedef {import("./Compiler")} Compiler *//** @typedef {import("./Stats")} Stats *//** @typedef {import("./Watching")} Watching *//** @typedef {import("./util/fs").InputFileSystem} InputFileSystem *//** @typedef {import("./util/fs").IntermediateFileSystem} IntermediateFileSystem *//** @typedef {import("./util/fs").OutputFileSystem} OutputFileSystem *//** @typedef {import("./util/fs").WatchFileSystem} WatchFileSystem *//** * @template T * @callback Callback * @param {(Error | null)=} err * @param {T=} result *//** * @callback RunWithDependenciesHandler * @param {Compiler} compiler * @param {Callback<MultiStats>} callback *//** * @typedef {Object} MultiCompilerOptions * @property {number=} parallelism how many Compilers are allows to run at the same time in parallel */module.exports = class MultiCompiler {	/**	 * @param {Compiler[] | Record<string, Compiler>} compilers child compilers	 * @param {MultiCompilerOptions} options options	 */	constructor(compilers, options) {		if (!Array.isArray(compilers)) {			compilers = Object.keys(compilers).map(name => {				compilers[name].name = name;				return compilers[name];			});		}		this.hooks = Object.freeze({			/** @type {SyncHook<[MultiStats]>} */			done: new SyncHook(["stats"]),			/** @type {MultiHook<SyncHook<[string | null, number]>>} */			invalid: new MultiHook(compilers.map(c => c.hooks.invalid)),			/** @type {MultiHook<AsyncSeriesHook<[Compiler]>>} */			run: new MultiHook(compilers.map(c => c.hooks.run)),			/** @type {SyncHook<[]>} */			watchClose: new SyncHook([]),			/** @type {MultiHook<AsyncSeriesHook<[Compiler]>>} */			watchRun: new MultiHook(compilers.map(c => c.hooks.watchRun)),			/** @type {MultiHook<SyncBailHook<[string, string, any[]], true>>} */			infrastructureLog: new MultiHook(				compilers.map(c => c.hooks.infrastructureLog)			)		});		this.compilers = compilers;		/** @type {MultiCompilerOptions} */		this._options = {			parallelism: options.parallelism || Infinity		};		/** @type {WeakMap<Compiler, string[]>} */		this.dependencies = new WeakMap();		this.running = false;		/** @type {Stats[]} */		const compilerStats = this.compilers.map(() => null);		let doneCompilers = 0;		for (let index = 0; index < this.compilers.length; index++) {			const compiler = this.compilers[index];			const compilerIndex = index;			let compilerDone = false;			compiler.hooks.done.tap("MultiCompiler", stats => {				if (!compilerDone) {					compilerDone = true;					doneCompilers++;				}				compilerStats[compilerIndex] = stats;				if (doneCompilers === this.compilers.length) {					this.hooks.done.call(new MultiStats(compilerStats));				}			});			compiler.hooks.invalid.tap("MultiCompiler", () => {				if (compilerDone) {					compilerDone = false;					doneCompilers--;				}			});		}	}	get options() {		return Object.assign(			this.compilers.map(c => c.options),			this._options		);	}	get outputPath() {		let commonPath = this.compilers[0].outputPath;		for (const compiler of this.compilers) {			while (				compiler.outputPath.indexOf(commonPath) !== 0 &&				/[/\\]/.test(commonPath)			) {				commonPath = commonPath.replace(/[/\\][^/\\]*$/, "");			}		}		if (!commonPath && this.compilers[0].outputPath[0] === "/") return "/";		return commonPath;	}	get inputFileSystem() {		throw new Error("Cannot read inputFileSystem of a MultiCompiler");	}	get outputFileSystem() {		throw new Error("Cannot read outputFileSystem of a MultiCompiler");	}	get watchFileSystem() {		throw new Error("Cannot read watchFileSystem of a MultiCompiler");	}	get intermediateFileSystem() {		throw new Error("Cannot read outputFileSystem of a MultiCompiler");	}	/**	 * @param {InputFileSystem} value the new input file system	 */	set inputFileSystem(value) {		for (const compiler of this.compilers) {			compiler.inputFileSystem = value;		}	}	/**	 * @param {OutputFileSystem} value the new output file system	 */	set outputFileSystem(value) {		for (const compiler of this.compilers) {			compiler.outputFileSystem = value;		}	}	/**	 * @param {WatchFileSystem} value the new watch file system	 */	set watchFileSystem(value) {		for (const compiler of this.compilers) {			compiler.watchFileSystem = value;		}	}	/**	 * @param {IntermediateFileSystem} value the new intermediate file system	 */	set intermediateFileSystem(value) {		for (const compiler of this.compilers) {			compiler.intermediateFileSystem = value;		}	}	getInfrastructureLogger(name) {		return this.compilers[0].getInfrastructureLogger(name);	}	/**	 * @param {Compiler} compiler the child compiler	 * @param {string[]} dependencies its dependencies	 * @returns {void}	 */	setDependencies(compiler, dependencies) {		this.dependencies.set(compiler, dependencies);	}	/**	 * @param {Callback<MultiStats>} callback signals when the validation is complete	 * @returns {boolean} true if the dependencies are valid	 */	validateDependencies(callback) {		/** @type {Set<{source: Compiler, target: Compiler}>} */		const edges = new Set();		/** @type {string[]} */		const missing = [];		const targetFound = compiler => {			for (const edge of edges) {				if (edge.target === compiler) {					return true;				}			}			return false;		};		const sortEdges = (e1, e2) => {			return (				e1.source.name.localeCompare(e2.source.name) ||				e1.target.name.localeCompare(e2.target.name)			);		};		for (const source of this.compilers) {			const dependencies = this.dependencies.get(source);			if (dependencies) {				for (const dep of dependencies) {					const target = this.compilers.find(c => c.name === dep);					if (!target) {						missing.push(dep);					} else {						edges.add({							source,							target						});					}				}			}		}		/** @type {string[]} */		const errors = missing.map(m => `Compiler dependency \`${m}\` not found.`);		const stack = this.compilers.filter(c => !targetFound(c));		while (stack.length > 0) {			const current = stack.pop();			for (const edge of edges) {				if (edge.source === current) {					edges.delete(edge);					const target = edge.target;					if (!targetFound(target)) {						stack.push(target);					}				}			}		}		if (edges.size > 0) {			/** @type {string[]} */			const lines = Array.from(edges)				.sort(sortEdges)				.map(edge => `${edge.source.name} -> ${edge.target.name}`);			lines.unshift("Circular dependency found in compiler dependencies.");			errors.unshift(lines.join("\n"));		}		if (errors.length > 0) {			const message = errors.join("\n");			callback(new Error(message));			return false;		}		return true;	}	// TODO webpack 6 remove	/**	 * @deprecated This method should have been private	 * @param {Compiler[]} compilers the child compilers	 * @param {RunWithDependenciesHandler} fn a handler to run for each compiler	 * @param {Callback<MultiStats>} callback the compiler's handler	 * @returns {void}	 */	runWithDependencies(compilers, fn, callback) {		const fulfilledNames = new Set();		let remainingCompilers = compilers;		const isDependencyFulfilled = d => fulfilledNames.has(d);		const getReadyCompilers = () => {			let readyCompilers = [];			let list = remainingCompilers;			remainingCompilers = [];			for (const c of list) {				const dependencies = this.dependencies.get(c);				const ready =					!dependencies || dependencies.every(isDependencyFulfilled);				if (ready) {					readyCompilers.push(c);				} else {					remainingCompilers.push(c);				}			}			return readyCompilers;		};		const runCompilers = callback => {			if (remainingCompilers.length === 0) return callback();			asyncLib.map(				getReadyCompilers(),				(compiler, callback) => {					fn(compiler, err => {						if (err) return callback(err);						fulfilledNames.add(compiler.name);						runCompilers(callback);					});				},				callback			);		};		runCompilers(callback);	}	/**	 * @template SetupResult	 * @param {function(Compiler, number, Callback<Stats>, function(): boolean, function(): void, function(): void): SetupResult} setup setup a single compiler	 * @param {function(Compiler, SetupResult, Callback<Stats>): void} run run/continue a single compiler	 * @param {Callback<MultiStats>} callback callback when all compilers are done, result includes Stats of all changed compilers	 * @returns {SetupResult[]} result of setup	 */	_runGraph(setup, run, callback) {		/** @typedef {{ compiler: Compiler, setupResult: SetupResult, result: Stats, state: "pending" | "blocked" | "queued" | "starting" | "running" | "running-outdated" | "done", children: Node[], parents: Node[] }} Node */		// State transitions for nodes:		// -> blocked (initial)		// blocked -> starting [running++] (when all parents done)		// queued -> starting [running++] (when processing the queue)		// starting -> running (when run has been called)		// running -> done [running--] (when compilation is done)		// done -> pending (when invalidated from file change)		// pending -> blocked [add to queue] (when invalidated from aggregated changes)		// done -> blocked [add to queue] (when invalidated, from parent invalidation)		// running -> running-outdated (when invalidated, either from change or parent invalidation)		// running-outdated -> blocked [running--] (when compilation is done)		/** @type {Node[]} */		const nodes = this.compilers.map(compiler => ({			compiler,			setupResult: undefined,			result: undefined,			state: "blocked",			children: [],			parents: []		}));		/** @type {Map<string, Node>} */		const compilerToNode = new Map();		for (const node of nodes) compilerToNode.set(node.compiler.name, node);		for (const node of nodes) {			const dependencies = this.dependencies.get(node.compiler);			if (!dependencies) continue;			for (const dep of dependencies) {				const parent = compilerToNode.get(dep);				node.parents.push(parent);				parent.children.push(node);			}		}		/** @type {ArrayQueue<Node>} */		const queue = new ArrayQueue();		for (const node of nodes) {			if (node.parents.length === 0) {				node.state = "queued";				queue.enqueue(node);			}		}		let errored = false;		let running = 0;		const parallelism = this._options.parallelism;		/**		 * @param {Node} node node		 * @param {Error=} err error		 * @param {Stats=} stats result		 * @returns {void}		 */		const nodeDone = (node, err, stats) => {			if (errored) return;			if (err) {				errored = true;				return asyncLib.each(					nodes,					(node, callback) => {						if (node.compiler.watching) {							node.compiler.watching.close(callback);						} else {							callback();						}					},					() => callback(err)				);			}			node.result = stats;			running--;			if (node.state === "running") {				node.state = "done";				for (const child of node.children) {					if (child.state === "blocked") queue.enqueue(child);				}			} else if (node.state === "running-outdated") {				node.state = "blocked";				queue.enqueue(node);			}			processQueue();		};		/**		 * @param {Node} node node		 * @returns {void}		 */		const nodeInvalidFromParent = node => {			if (node.state === "done") {				node.state = "blocked";			} else if (node.state === "running") {				node.state = "running-outdated";			}			for (const child of node.children) {				nodeInvalidFromParent(child);			}		};		/**		 * @param {Node} node node		 * @returns {void}		 */		const nodeInvalid = node => {			if (node.state === "done") {				node.state = "pending";			} else if (node.state === "running") {				node.state = "running-outdated";			}			for (const child of node.children) {				nodeInvalidFromParent(child);			}		};		/**		 * @param {Node} node node		 * @returns {void}		 */		const nodeChange = node => {			nodeInvalid(node);			if (node.state === "pending") {				node.state = "blocked";			}			if (node.state === "blocked") {				queue.enqueue(node);				processQueue();			}		};		const setupResults = [];		nodes.forEach((node, i) => {			setupResults.push(				(node.setupResult = setup(					node.compiler,					i,					nodeDone.bind(null, node),					() => node.state !== "starting" && node.state !== "running",					() => nodeChange(node),					() => nodeInvalid(node)				))			);		});		let processing = true;		const processQueue = () => {			if (processing) return;			processing = true;			process.nextTick(processQueueWorker);		};		const processQueueWorker = () => {			while (running < parallelism && queue.length > 0 && !errored) {				const node = queue.dequeue();				if (					node.state === "queued" ||					(node.state === "blocked" &&						node.parents.every(p => p.state === "done"))				) {					running++;					node.state = "starting";					run(node.compiler, node.setupResult, nodeDone.bind(null, node));					node.state = "running";				}			}			processing = false;			if (				!errored &&				running === 0 &&				nodes.every(node => node.state === "done")			) {				const stats = [];				for (const node of nodes) {					const result = node.result;					if (result) {						node.result = undefined;						stats.push(result);					}				}				if (stats.length > 0) {					callback(null, new MultiStats(stats));				}			}		};		processQueueWorker();		return setupResults;	}	/**	 * @param {WatchOptions|WatchOptions[]} watchOptions the watcher's options	 * @param {Callback<MultiStats>} handler signals when the call finishes	 * @returns {MultiWatching} a compiler watcher	 */	watch(watchOptions, handler) {		if (this.running) {			return handler(new ConcurrentCompilationError());		}		this.running = true;		if (this.validateDependencies(handler)) {			const watchings = this._runGraph(				(compiler, idx, callback, isBlocked, setChanged, setInvalid) => {					const watching = compiler.watch(						Array.isArray(watchOptions) ? watchOptions[idx] : watchOptions,						callback					);					if (watching) {						watching._onInvalid = setInvalid;						watching._onChange = setChanged;						watching._isBlocked = isBlocked;					}					return watching;				},				(compiler, watching, callback) => {					if (compiler.watching !== watching) return;					if (!watching.running) watching.invalidate();				},				handler			);			return new MultiWatching(watchings, this);		}		return new MultiWatching([], this);	}	/**	 * @param {Callback<MultiStats>} callback signals when the call finishes	 * @returns {void}	 */	run(callback) {		if (this.running) {			return callback(new ConcurrentCompilationError());		}		this.running = true;		if (this.validateDependencies(callback)) {			this._runGraph(				() => {},				(compiler, setupResult, callback) => compiler.run(callback),				(err, stats) => {					this.running = false;					if (callback !== undefined) {						return callback(err, stats);					}				}			);		}	}	purgeInputFileSystem() {		for (const compiler of this.compilers) {			if (compiler.inputFileSystem && compiler.inputFileSystem.purge) {				compiler.inputFileSystem.purge();			}		}	}	/**	 * @param {Callback<void>} callback signals when the compiler closes	 * @returns {void}	 */	close(callback) {		asyncLib.each(			this.compilers,			(compiler, callback) => {				compiler.close(callback);			},			callback		);	}};
 |