webpack/lib/MultiCompiler.js

538 lines
14 KiB
JavaScript
Raw Normal View History

/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
2018-07-30 23:08:51 +08:00
2017-06-12 03:45:55 +08:00
"use strict";
2018-02-11 12:27:09 +08:00
const asyncLib = require("neo-async");
2018-07-30 23:08:51 +08:00
const { SyncHook, MultiHook } = require("tapable");
2018-12-09 19:54:17 +08:00
const ConcurrentCompilationError = require("./ConcurrentCompilationError");
2018-07-30 23:08:51 +08:00
const MultiStats = require("./MultiStats");
const MultiWatching = require("./MultiWatching");
const ArrayQueue = require("./util/ArrayQueue");
2019-08-05 18:15:03 +08:00
/** @template T @typedef {import("tapable").AsyncSeriesHook<T>} AsyncSeriesHook<T> */
/** @template T @template R @typedef {import("tapable").SyncBailHook<T, R>} SyncBailHook<T, R> */
2018-12-10 18:34:59 +08:00
/** @typedef {import("../declarations/WebpackOptions").WatchOptions} WatchOptions */
2018-12-09 21:26:35 +08:00
/** @typedef {import("./Compiler")} Compiler */
2018-12-10 18:34:59 +08:00
/** @typedef {import("./Stats")} Stats */
2018-12-09 19:54:17 +08:00
/** @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 */
2018-12-09 21:26:35 +08:00
2018-12-10 18:34:59 +08:00
/**
* @template T
* @callback Callback
* @param {Error=} 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
*/
2018-06-26 14:27:44 +08:00
module.exports = class MultiCompiler {
2018-12-09 21:26:35 +08:00
/**
2018-12-10 18:34:59 +08:00
* @param {Compiler[] | Record<string, Compiler>} compilers child compilers
* @param {MultiCompilerOptions} options options
2018-12-09 21:26:35 +08:00
*/
constructor(compilers, options) {
2018-12-10 18:34:59 +08:00
if (!Array.isArray(compilers)) {
compilers = Object.keys(compilers).map(name => {
compilers[name].name = name;
return compilers[name];
});
}
2018-07-30 20:25:40 +08:00
this.hooks = Object.freeze({
2018-12-09 19:54:17 +08:00
/** @type {SyncHook<[MultiStats]>} */
2017-11-28 17:18:46 +08:00
done: new SyncHook(["stats"]),
/** @type {MultiHook<SyncHook<[string | null, number]>>} */
invalid: new MultiHook(compilers.map(c => c.hooks.invalid)),
2018-12-09 19:54:17 +08:00
/** @type {MultiHook<AsyncSeriesHook<[Compiler]>>} */
run: new MultiHook(compilers.map(c => c.hooks.run)),
2018-12-09 19:54:17 +08:00
/** @type {SyncHook<[]>} */
watchClose: new SyncHook([]),
2018-12-09 19:54:17 +08:00
/** @type {MultiHook<AsyncSeriesHook<[Compiler]>>} */
watchRun: new MultiHook(compilers.map(c => c.hooks.watchRun)),
2019-08-05 18:15:03 +08:00
/** @type {MultiHook<SyncBailHook<[string, string, any[]], true>>} */
infrastructureLog: new MultiHook(
compilers.map(c => c.hooks.infrastructureLog)
)
2018-07-30 20:25:40 +08:00
});
2017-06-12 03:45:55 +08:00
this.compilers = compilers;
/** @type {MultiCompilerOptions} */
this._options = {
parallelism: options.parallelism || Infinity
};
2018-12-09 21:26:35 +08:00
/** @type {WeakMap<Compiler, string[]>} */
this.dependencies = new WeakMap();
2018-12-10 18:34:59 +08:00
this.running = false;
/** @type {Stats[]} */
const compilerStats = this.compilers.map(() => null);
2017-06-12 03:45:55 +08:00
let doneCompilers = 0;
2018-12-10 18:34:59 +08:00
for (let index = 0; index < this.compilers.length; index++) {
const compiler = this.compilers[index];
const compilerIndex = index;
2017-06-12 03:45:55 +08:00
let compilerDone = false;
2018-02-25 09:15:37 +08:00
// eslint-disable-next-line no-loop-func
2018-02-25 09:00:20 +08:00
compiler.hooks.done.tap("MultiCompiler", stats => {
if (!compilerDone) {
2017-06-12 03:45:55 +08:00
compilerDone = true;
doneCompilers++;
}
2018-01-22 20:52:43 +08:00
compilerStats[compilerIndex] = stats;
2018-02-25 09:00:20 +08:00
if (doneCompilers === this.compilers.length) {
2017-11-28 17:18:46 +08:00
this.hooks.done.call(new MultiStats(compilerStats));
2017-06-12 03:45:55 +08:00
}
});
2018-02-25 09:15:37 +08:00
// eslint-disable-next-line no-loop-func
2018-02-25 09:00:20 +08:00
compiler.hooks.invalid.tap("MultiCompiler", () => {
if (compilerDone) {
2017-06-12 03:45:55 +08:00
compilerDone = false;
doneCompilers--;
}
});
2018-01-22 20:52:43 +08:00
}
}
2015-04-24 05:55:50 +08:00
get options() {
return Object.assign(
this.compilers.map(c => c.options),
this._options
);
}
2017-06-12 03:45:55 +08:00
get outputPath() {
let commonPath = this.compilers[0].outputPath;
2018-02-25 09:00:20 +08:00
for (const compiler of this.compilers) {
while (
compiler.outputPath.indexOf(commonPath) !== 0 &&
/[/\\]/.test(commonPath)
) {
2017-07-24 17:54:06 +08:00
commonPath = commonPath.replace(/[/\\][^/\\]*$/, "");
2014-06-19 05:02:33 +08:00
}
}
2018-02-25 09:00:20 +08:00
if (!commonPath && this.compilers[0].outputPath[0] === "/") return "/";
2017-06-12 03:45:55 +08:00
return commonPath;
}
2017-06-12 03:45:55 +08:00
get inputFileSystem() {
throw new Error("Cannot read inputFileSystem of a MultiCompiler");
}
2017-06-12 03:45:55 +08:00
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
*/
2017-06-12 03:45:55 +08:00
set inputFileSystem(value) {
2018-02-25 09:00:20 +08:00
for (const compiler of this.compilers) {
2017-06-12 03:45:55 +08:00
compiler.inputFileSystem = value;
2018-01-22 20:52:43 +08:00
}
}
/**
* @param {OutputFileSystem} value the new output file system
*/
2017-06-12 03:45:55 +08:00
set outputFileSystem(value) {
2018-02-25 09:00:20 +08:00
for (const compiler of this.compilers) {
2017-06-12 03:45:55 +08:00
compiler.outputFileSystem = value;
2018-01-22 20:52:43 +08:00
}
}
/**
* @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);
}
2018-12-09 21:26:35 +08:00
/**
* @param {Compiler} compiler the child compiler
* @param {string[]} dependencies its dependencies
* @returns {void}
*/
setDependencies(compiler, dependencies) {
this.dependencies.set(compiler, dependencies);
}
2018-12-10 18:34:59 +08:00
/**
* @param {Callback<MultiStats>} callback signals when the validation is complete
* @returns {boolean} true if the dependencies are valid
*/
validateDependencies(callback) {
2018-12-10 18:34:59 +08:00
/** @type {Set<{source: Compiler, target: Compiler}>} */
2017-09-17 22:59:35 +08:00
const edges = new Set();
2018-12-10 18:34:59 +08:00
/** @type {string[]} */
const missing = [];
2018-02-25 09:00:20 +08:00
const targetFound = compiler => {
for (const edge of edges) {
if (edge.target === compiler) {
2017-09-17 22:59:35 +08:00
return true;
}
}
return false;
};
const sortEdges = (e1, e2) => {
2018-02-25 09:00:20 +08:00
return (
e1.source.name.localeCompare(e2.source.name) ||
e1.target.name.localeCompare(e2.target.name)
);
};
2018-02-25 09:00:20 +08:00
for (const source of this.compilers) {
2018-12-09 21:26:35 +08:00
const dependencies = this.dependencies.get(source);
if (dependencies) {
for (const dep of dependencies) {
2018-02-25 09:00:20 +08:00
const target = this.compilers.find(c => c.name === dep);
if (!target) {
2017-09-09 00:46:47 +08:00
missing.push(dep);
} else {
2017-09-17 22:59:35 +08:00
edges.add({
2017-09-09 00:46:47 +08:00
source,
target
});
}
}
}
}
2021-01-13 23:48:25 +08:00
/** @type {string[]} */
2018-02-25 09:00:20 +08:00
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();
2018-02-25 09:00:20 +08:00
for (const edge of edges) {
if (edge.source === current) {
2017-09-17 22:59:35 +08:00
edges.delete(edge);
const target = edge.target;
2018-02-25 09:00:20 +08:00
if (!targetFound(target)) {
2017-09-17 22:59:35 +08:00
stack.push(target);
}
}
}
}
2018-02-25 09:00:20 +08:00
if (edges.size > 0) {
2021-01-13 23:48:25 +08:00
/** @type {string[]} */
2018-02-25 09:00:20 +08:00
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"));
}
2018-02-25 09:00:20 +08:00
if (errors.length > 0) {
const message = errors.join("\n");
callback(new Error(message));
return false;
}
return true;
}
// TODO webpack 6 remove
2018-12-10 18:34:59 +08:00
/**
* @deprecated This method should have been private
2018-12-10 18:34:59 +08:00
* @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}
*/
2017-06-12 03:45:55 +08:00
runWithDependencies(compilers, fn, callback) {
2018-01-05 14:41:09 +08:00
const fulfilledNames = new Set();
2017-06-12 03:45:55 +08:00
let remainingCompilers = compilers;
2018-02-25 09:00:20 +08:00
const isDependencyFulfilled = d => fulfilledNames.has(d);
2017-06-12 03:45:55 +08:00
const getReadyCompilers = () => {
let readyCompilers = [];
let list = remainingCompilers;
remainingCompilers = [];
2018-02-25 09:00:20 +08:00
for (const c of list) {
2018-12-09 21:26:35 +08:00
const dependencies = this.dependencies.get(c);
2018-02-25 09:00:20 +08:00
const ready =
2018-12-09 21:26:35 +08:00
!dependencies || dependencies.every(isDependencyFulfilled);
if (ready) {
readyCompilers.push(c);
} else {
remainingCompilers.push(c);
}
2017-06-12 03:45:55 +08:00
}
return readyCompilers;
};
2018-02-25 09:00:20 +08:00
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
);
2017-06-12 03:45:55 +08:00
};
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, 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, result: Stats, state: "blocked" | "queued" | "running" | "done", children: Node[], parents: Node[] }} Node */
/** @type {Node[]} */
const nodes = this.compilers.map(compiler => ({
compiler,
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);
}
}
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;
2021-02-15 04:43:30 +08:00
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") continue;
if (child.parents.every(p => p.state === "done")) {
child.state = "queued";
queue.enqueue(child);
}
}
process.nextTick(processQueue);
};
/**
* @param {Node} node node
* @returns {void}
*/
const nodeInvalid = node => {
if (node.state === "done" || node.state === "running") {
node.state = "blocked";
}
for (const child of node.children) {
nodeInvalid(child);
}
};
/**
* @param {Node} node node
* @returns {void}
*/
const nodeChange = node => {
nodeInvalid(node);
if (
node.state === "blocked" &&
node.parents.every(p => p.state === "done")
) {
node.state = "queued";
queue.enqueue(node);
processQueue();
}
};
const setupResults = [];
nodes.forEach((node, i) => {
setupResults.push(
setup(
node.compiler,
i,
nodeDone.bind(null, node),
() => node.state === "blocked" || node.state === "queued",
() => nodeChange(node),
() => nodeInvalid(node)
)
);
});
const processQueue = () => {
while (running < parallelism && queue.length > 0 && !errored) {
const node = queue.dequeue();
if (node.state !== "queued") continue;
running++;
node.state = "running";
run(node.compiler, nodeDone.bind(null, node));
}
if (!errored && running === 0) {
2021-02-15 04:57:52 +08:00
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));
}
}
};
processQueue();
return setupResults;
}
2018-12-10 18:34:59 +08:00
/**
* @param {WatchOptions|WatchOptions[]} watchOptions the watcher's options
2018-12-10 18:34:59 +08:00
* @param {Callback<MultiStats>} handler signals when the call finishes
* @returns {MultiWatching} a compiler watcher
*/
2017-06-12 03:45:55 +08:00
watch(watchOptions, handler) {
2018-12-09 21:26:35 +08:00
if (this.running) {
return handler(new ConcurrentCompilationError());
}
this.running = true;
2018-12-09 21:26:35 +08:00
2018-03-13 18:16:39 +08:00
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
2018-02-25 09:00:20 +08:00
);
if (watching) {
watching._onInvalid = setInvalid;
watching._onChange = setChanged;
watching._isBlocked = isBlocked;
}
return watching;
2018-02-25 09:00:20 +08:00
},
(compiler, initial, callback) => {
if (!compiler.watching.running) compiler.watching.invalidate();
},
handler
2018-02-25 09:00:20 +08:00
);
return new MultiWatching(watchings, this);
}
return new MultiWatching([], this);
2017-06-12 03:45:55 +08:00
}
2018-12-10 18:34:59 +08:00
/**
* @param {Callback<MultiStats>} callback signals when the call finishes
* @returns {void}
*/
2017-06-12 03:45:55 +08:00
run(callback) {
if (this.running) {
return callback(new ConcurrentCompilationError());
}
this.running = true;
2018-02-25 09:00:20 +08:00
if (this.validateDependencies(callback)) {
this._runGraph(
() => {},
(compiler, callback) => compiler.run(callback),
(err, stats) => {
this.running = false;
if (callback !== undefined) {
return callback(err, stats);
}
2018-02-25 09:00:20 +08:00
}
);
}
2017-06-12 03:45:55 +08:00
}
2017-06-12 03:45:55 +08:00
purgeInputFileSystem() {
2018-02-25 09:00:20 +08:00
for (const compiler of this.compilers) {
if (compiler.inputFileSystem && compiler.inputFileSystem.purge) {
2017-06-12 03:45:55 +08:00
compiler.inputFileSystem.purge();
}
2018-01-22 20:52:43 +08:00
}
2017-06-12 03:45:55 +08:00
}
2018-12-10 18:34:59 +08:00
/**
* @param {Callback<void>} callback signals when the compiler closes
* @returns {void}
*/
close(callback) {
asyncLib.each(
this.compilers,
(compiler, callback) => {
compiler.close(callback);
},
callback
);
}
2014-06-12 04:52:02 +08:00
};