MultiCompiler.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const asyncLib = require("neo-async");
  7. const { MultiHook, SyncHook } = require("tapable");
  8. const MultiStats = require("./MultiStats");
  9. const MultiWatching = require("./MultiWatching");
  10. const WebpackError = require("./errors/WebpackError");
  11. const ArrayQueue = require("./util/ArrayQueue");
  12. const memoize = require("./util/memoize");
  13. const getConcurrentCompilationError = memoize(() =>
  14. require("./errors/ConcurrentCompilationError")
  15. );
  16. /**
  17. * Defines the shared type used by this module.
  18. * @template T
  19. * @typedef {import("tapable").AsyncSeriesHook<T>} AsyncSeriesHook<T>
  20. */
  21. /**
  22. * Defines the shared type used by this module.
  23. * @template T
  24. * @template R
  25. * @typedef {import("tapable").SyncBailHook<T, R>} SyncBailHook<T, R>
  26. */
  27. /**
  28. * @import {
  29. * WebpackOptions,
  30. * WatchOptions
  31. * } from "../declarations/WebpackOptions"
  32. */
  33. /** @import Compiler from "./Compiler" */
  34. /**
  35. * Defines the callback type used by this module.
  36. * @template T
  37. * @template [R=void]
  38. * @typedef {import("./webpack").Callback<T, R>} Callback
  39. */
  40. /** @import { ErrorCallback } from "./webpack" */
  41. /** @import Stats from "./Stats" */
  42. /** @import { Logger } from "./logging/Logger" */
  43. /**
  44. * @import {
  45. * InputFileSystem,
  46. * IntermediateFileSystem,
  47. * OutputFileSystem,
  48. * WatchFileSystem
  49. * } from "./util/fs"
  50. */
  51. /**
  52. * Defines the run with dependencies handler callback.
  53. * @callback RunWithDependenciesHandler
  54. * @param {Compiler} compiler
  55. * @param {Callback<MultiStats>} callback
  56. * @returns {void}
  57. */
  58. /**
  59. * Defines the multi compiler options type used by this module.
  60. * @typedef {object} MultiCompilerOptions
  61. * @property {number=} parallelism how many Compilers are allows to run at the same time in parallel
  62. */
  63. /** @typedef {ReadonlyArray<WebpackOptions> & MultiCompilerOptions} MultiWebpackOptions */
  64. const CLASS_NAME = "MultiCompiler";
  65. module.exports = class MultiCompiler {
  66. /**
  67. * Creates an instance of MultiCompiler.
  68. * @param {Compiler[] | Record<string, Compiler>} compilers child compilers
  69. * @param {MultiCompilerOptions} options options
  70. */
  71. constructor(compilers, options) {
  72. if (!Array.isArray(compilers)) {
  73. /** @type {Compiler[]} */
  74. compilers = Object.keys(compilers).map((name) => {
  75. /** @type {Record<string, Compiler>} */
  76. (compilers)[name].name = name;
  77. return /** @type {Record<string, Compiler>} */ (compilers)[name];
  78. });
  79. }
  80. this.hooks = Object.freeze({
  81. /** @type {SyncHook<[MultiStats, Compiler[]]>} */
  82. done: new SyncHook(["stats", "changedCompilers"]),
  83. /** @type {MultiHook<SyncHook<[string | null, number]>>} */
  84. invalid: new MultiHook(compilers.map((c) => c.hooks.invalid)),
  85. /** @type {MultiHook<AsyncSeriesHook<[Compiler]>>} */
  86. run: new MultiHook(compilers.map((c) => c.hooks.run)),
  87. /** @type {SyncHook<[]>} */
  88. watchClose: new SyncHook([]),
  89. /** @type {MultiHook<AsyncSeriesHook<[]>>} */
  90. shutdown: new MultiHook(compilers.map((c) => c.hooks.shutdown)),
  91. /** @type {MultiHook<AsyncSeriesHook<[Compiler]>>} */
  92. watchRun: new MultiHook(compilers.map((c) => c.hooks.watchRun)),
  93. /** @type {MultiHook<SyncBailHook<[string, string, EXPECTED_ANY[] | undefined], true | void>>} */
  94. infrastructureLog: new MultiHook(
  95. compilers.map((c) => c.hooks.infrastructureLog)
  96. )
  97. });
  98. /** @type {Compiler[]} */
  99. this.compilers = compilers;
  100. /** @type {MultiCompilerOptions} */
  101. this._options = {
  102. parallelism: options.parallelism || Infinity
  103. };
  104. /** @type {WeakMap<Compiler, string[]>} */
  105. this.dependencies = new WeakMap();
  106. /** @type {boolean} */
  107. this.running = false;
  108. /** @type {MultiWatching | undefined} */
  109. this.watching = undefined;
  110. /** @type {(Stats | null)[]} */
  111. const compilerStats = this.compilers.map(() => null);
  112. /** @type {Set<Compiler>} */
  113. const changedCompilers = new Set();
  114. let doneCompilers = 0;
  115. for (let index = 0; index < this.compilers.length; index++) {
  116. const compiler = this.compilers[index];
  117. const compilerIndex = index;
  118. let compilerDone = false;
  119. // eslint-disable-next-line no-loop-func
  120. compiler.hooks.done.tap(CLASS_NAME, (stats) => {
  121. changedCompilers.add(compiler);
  122. if (!compilerDone) {
  123. compilerDone = true;
  124. doneCompilers++;
  125. }
  126. compilerStats[compilerIndex] = stats;
  127. if (doneCompilers === this.compilers.length) {
  128. const changed = this.compilers.filter((c) => changedCompilers.has(c));
  129. changedCompilers.clear();
  130. this.hooks.done.call(
  131. // Copy: the live array is cleared per child on shutdown.
  132. new MultiStats(/** @type {Stats[]} */ ([...compilerStats])),
  133. changed
  134. );
  135. }
  136. });
  137. // Each entry holds a Stats -> Compilation; don't outlive the child's close().
  138. compiler.hooks.shutdown.tap(CLASS_NAME, () => {
  139. compilerStats[compilerIndex] = null;
  140. });
  141. // eslint-disable-next-line no-loop-func
  142. compiler.hooks.invalid.tap(CLASS_NAME, () => {
  143. if (compilerDone) {
  144. compilerDone = false;
  145. doneCompilers--;
  146. }
  147. });
  148. // Release fields on this child's Compilation once it's done. The
  149. // stage: Infinity tap runs after every afterDone tap at a lower
  150. // stage, so plugins observing compilation state in afterDone still
  151. // see it intact. Stats remains usable; only fields Stats never reads
  152. // (and that the persistent cache never serializes) are dropped.
  153. compiler.hooks.afterDone.tap(
  154. { name: CLASS_NAME, stage: Infinity },
  155. (stats) => {
  156. if (stats !== undefined) {
  157. compiler._releaseUnusedCompilationData(stats.compilation);
  158. }
  159. }
  160. );
  161. }
  162. this._validateCompilersOptions();
  163. }
  164. _validateCompilersOptions() {
  165. if (this.compilers.length < 2) return;
  166. /**
  167. * Adds the provided compiler to the multi compiler.
  168. * @param {Compiler} compiler compiler
  169. * @param {WebpackError} warning warning
  170. */
  171. const addWarning = (compiler, warning) => {
  172. compiler.hooks.thisCompilation.tap(CLASS_NAME, (compilation) => {
  173. compilation.warnings.push(warning);
  174. });
  175. };
  176. /** @type {Set<string>} */
  177. const cacheNames = new Set();
  178. for (const compiler of this.compilers) {
  179. if (compiler.options.cache && "name" in compiler.options.cache) {
  180. const name = /** @type {string} */ (compiler.options.cache.name);
  181. if (cacheNames.has(name)) {
  182. addWarning(
  183. compiler,
  184. new WebpackError(
  185. `${
  186. compiler.name
  187. ? `Compiler with name "${compiler.name}" doesn't use unique cache name. `
  188. : ""
  189. }Please set unique "cache.name" option. Name "${name}" already used.`
  190. )
  191. );
  192. } else {
  193. cacheNames.add(name);
  194. }
  195. }
  196. }
  197. }
  198. get options() {
  199. return Object.assign(
  200. this.compilers.map((c) => c.options),
  201. this._options
  202. );
  203. }
  204. get outputPath() {
  205. // Match whole path segments: "/dist-app" must not count as inside "/dist"
  206. /**
  207. * @param {string} parent candidate ancestor path
  208. * @param {string} path path to test
  209. * @returns {boolean} true when path is parent or inside it
  210. */
  211. const isSubPath = (parent, path) => {
  212. if (path === parent) return true;
  213. if (!path.startsWith(parent)) return false;
  214. const next = path.charCodeAt(parent.length);
  215. return next === 47 || next === 92 || /[/\\]$/.test(parent);
  216. };
  217. let commonPath = this.compilers[0].outputPath;
  218. for (const compiler of this.compilers) {
  219. while (
  220. !isSubPath(commonPath, compiler.outputPath) &&
  221. /[/\\]/.test(commonPath)
  222. ) {
  223. commonPath = commonPath.replace(/[/\\][^/\\]*$/, "");
  224. }
  225. }
  226. if (!commonPath && this.compilers[0].outputPath[0] === "/") return "/";
  227. return commonPath;
  228. }
  229. get inputFileSystem() {
  230. throw new Error("Cannot read inputFileSystem of a MultiCompiler");
  231. }
  232. /**
  233. * Sets input file system.
  234. * @param {InputFileSystem} value the new input file system
  235. */
  236. set inputFileSystem(value) {
  237. for (const compiler of this.compilers) {
  238. compiler.inputFileSystem = value;
  239. }
  240. }
  241. get outputFileSystem() {
  242. throw new Error("Cannot read outputFileSystem of a MultiCompiler");
  243. }
  244. /**
  245. * Sets output file system.
  246. * @param {OutputFileSystem} value the new output file system
  247. */
  248. set outputFileSystem(value) {
  249. for (const compiler of this.compilers) {
  250. compiler.outputFileSystem = value;
  251. }
  252. }
  253. get watchFileSystem() {
  254. throw new Error("Cannot read watchFileSystem of a MultiCompiler");
  255. }
  256. /**
  257. * Sets watch file system.
  258. * @param {WatchFileSystem} value the new watch file system
  259. */
  260. set watchFileSystem(value) {
  261. for (const compiler of this.compilers) {
  262. compiler.watchFileSystem = value;
  263. }
  264. }
  265. /**
  266. * Sets intermediate file system.
  267. * @param {IntermediateFileSystem} value the new intermediate file system
  268. */
  269. set intermediateFileSystem(value) {
  270. for (const compiler of this.compilers) {
  271. compiler.intermediateFileSystem = value;
  272. }
  273. }
  274. get intermediateFileSystem() {
  275. throw new Error("Cannot read outputFileSystem of a MultiCompiler");
  276. }
  277. /**
  278. * Gets infrastructure logger.
  279. * @param {string | (() => string)} name name of the logger, or function called once to get the logger name
  280. * @returns {Logger} a logger with that name
  281. */
  282. getInfrastructureLogger(name) {
  283. return this.compilers[0].getInfrastructureLogger(name);
  284. }
  285. /**
  286. * Updates dependencies using the provided compiler.
  287. * @param {Compiler} compiler the child compiler
  288. * @param {string[]} dependencies its dependencies
  289. * @returns {void}
  290. */
  291. setDependencies(compiler, dependencies) {
  292. this.dependencies.set(compiler, dependencies);
  293. }
  294. /**
  295. * Validate dependencies.
  296. * @param {Callback<MultiStats>} callback signals when the validation is complete
  297. * @returns {boolean} true if the dependencies are valid
  298. */
  299. validateDependencies(callback) {
  300. /** @type {Set<{ source: Compiler, target: Compiler }>} */
  301. const edges = new Set();
  302. /** @type {string[]} */
  303. const missing = [];
  304. /**
  305. * Returns target was found.
  306. * @param {Compiler} compiler compiler
  307. * @returns {boolean} target was found
  308. */
  309. const targetFound = (compiler) => {
  310. for (const edge of edges) {
  311. if (edge.target === compiler) {
  312. return true;
  313. }
  314. }
  315. return false;
  316. };
  317. /**
  318. * Returns result.
  319. * @param {{ source: Compiler, target: Compiler }} e1 edge 1
  320. * @param {{ source: Compiler, target: Compiler }} e2 edge 2
  321. * @returns {number} result
  322. */
  323. const sortEdges = (e1, e2) =>
  324. /** @type {string} */
  325. (e1.source.name).localeCompare(/** @type {string} */ (e2.source.name)) ||
  326. /** @type {string} */
  327. (e1.target.name).localeCompare(/** @type {string} */ (e2.target.name));
  328. for (const source of this.compilers) {
  329. const dependencies = this.dependencies.get(source);
  330. if (dependencies) {
  331. for (const dep of dependencies) {
  332. const target = this.compilers.find((c) => c.name === dep);
  333. if (!target) {
  334. missing.push(dep);
  335. } else {
  336. edges.add({
  337. source,
  338. target
  339. });
  340. }
  341. }
  342. }
  343. }
  344. /** @type {string[]} */
  345. const errors = missing.map(
  346. (m) => `Compiler dependency \`${m}\` not found.`
  347. );
  348. const stack = this.compilers.filter((c) => !targetFound(c));
  349. while (stack.length > 0) {
  350. const current = stack.pop();
  351. for (const edge of edges) {
  352. if (edge.source === current) {
  353. edges.delete(edge);
  354. const target = edge.target;
  355. if (!targetFound(target)) {
  356. stack.push(target);
  357. }
  358. }
  359. }
  360. }
  361. if (edges.size > 0) {
  362. /** @type {string[]} */
  363. const lines = [...edges]
  364. .sort(sortEdges)
  365. .map((edge) => `${edge.source.name} -> ${edge.target.name}`);
  366. lines.unshift("Circular dependency found in compiler dependencies.");
  367. errors.unshift(lines.join("\n"));
  368. }
  369. if (errors.length > 0) {
  370. const message = errors.join("\n");
  371. callback(new Error(message));
  372. return false;
  373. }
  374. return true;
  375. }
  376. // TODO webpack 6 remove
  377. /**
  378. * Run with dependencies.
  379. * @deprecated This method should have been private
  380. * @param {Compiler[]} compilers the child compilers
  381. * @param {RunWithDependenciesHandler} fn a handler to run for each compiler
  382. * @param {Callback<Stats[]>} callback the compiler's handler
  383. * @returns {void}
  384. */
  385. runWithDependencies(compilers, fn, callback) {
  386. /** @type {Set<string>} */
  387. const fulfilledNames = new Set();
  388. let remainingCompilers = compilers;
  389. /**
  390. * Checks whether this multi compiler is dependency fulfilled.
  391. * @param {string} d dependency
  392. * @returns {boolean} when dependency was fulfilled
  393. */
  394. const isDependencyFulfilled = (d) => fulfilledNames.has(d);
  395. /**
  396. * Gets ready compilers.
  397. * @returns {Compiler[]} compilers
  398. */
  399. const getReadyCompilers = () => {
  400. /** @type {Compiler[]} */
  401. const readyCompilers = [];
  402. const list = remainingCompilers;
  403. remainingCompilers = [];
  404. for (const c of list) {
  405. const dependencies = this.dependencies.get(c);
  406. const ready =
  407. !dependencies || dependencies.every(isDependencyFulfilled);
  408. if (ready) {
  409. readyCompilers.push(c);
  410. } else {
  411. remainingCompilers.push(c);
  412. }
  413. }
  414. return readyCompilers;
  415. };
  416. /**
  417. * Processes the provided stat.
  418. * @param {Callback<Stats[]>} callback callback
  419. * @returns {void}
  420. */
  421. const runCompilers = (callback) => {
  422. if (remainingCompilers.length === 0) return callback(null);
  423. asyncLib.map(
  424. getReadyCompilers(),
  425. (compiler, callback) => {
  426. fn(compiler, (err) => {
  427. if (err) return callback(err);
  428. fulfilledNames.add(/** @type {string} */ (compiler.name));
  429. runCompilers(callback);
  430. });
  431. },
  432. (err, results) => {
  433. callback(/** @type {Error | null} */ (err), results);
  434. }
  435. );
  436. };
  437. runCompilers(callback);
  438. }
  439. /**
  440. * Returns result of setup.
  441. * @template SetupResult
  442. * @param {(compiler: Compiler, index: number, doneCallback: Callback<Stats>, isBlocked: () => boolean, setChanged: () => void, setInvalid: () => void) => SetupResult} setup setup a single compiler
  443. * @param {(compiler: Compiler, setupResult: SetupResult, callback: Callback<Stats>) => void} run run/continue a single compiler
  444. * @param {Callback<MultiStats>} callback callback when all compilers are done, result includes Stats of all changed compilers
  445. * @returns {SetupResult[]} result of setup
  446. */
  447. _runGraph(setup, run, callback) {
  448. /** @typedef {{ compiler: Compiler, setupResult: undefined | SetupResult, result: undefined | Stats, state: "pending" | "blocked" | "queued" | "starting" | "running" | "running-outdated" | "done", children: Node[], parents: Node[] }} Node */
  449. // State transitions for nodes:
  450. // -> blocked (initial)
  451. // blocked -> starting [running++] (when all parents done)
  452. // queued -> starting [running++] (when processing the queue)
  453. // starting -> running (when run has been called)
  454. // running -> done [running--] (when compilation is done)
  455. // done -> pending (when invalidated from file change)
  456. // pending -> blocked [add to queue] (when invalidated from aggregated changes)
  457. // done -> blocked [add to queue] (when invalidated, from parent invalidation)
  458. // running -> running-outdated (when invalidated, either from change or parent invalidation)
  459. // running-outdated -> blocked [running--] (when compilation is done)
  460. /** @type {Node[]} */
  461. const nodes = this.compilers.map((compiler) => ({
  462. compiler,
  463. setupResult: undefined,
  464. result: undefined,
  465. state: "blocked",
  466. children: [],
  467. parents: []
  468. }));
  469. /** @type {Map<string, Node>} */
  470. const compilerToNode = new Map();
  471. for (const node of nodes) {
  472. compilerToNode.set(/** @type {string} */ (node.compiler.name), node);
  473. }
  474. for (const node of nodes) {
  475. const dependencies = this.dependencies.get(node.compiler);
  476. if (!dependencies) continue;
  477. for (const dep of dependencies) {
  478. const parent = /** @type {Node} */ (compilerToNode.get(dep));
  479. node.parents.push(parent);
  480. parent.children.push(node);
  481. }
  482. }
  483. /** @type {ArrayQueue<Node>} */
  484. const queue = new ArrayQueue();
  485. for (const node of nodes) {
  486. if (node.parents.length === 0) {
  487. node.state = "queued";
  488. queue.enqueue(node);
  489. }
  490. }
  491. let errored = false;
  492. let running = 0;
  493. let duringSetup = true;
  494. /** @type {Error | null} */
  495. let setupError = null;
  496. const parallelism = /** @type {number} */ (this._options.parallelism);
  497. /**
  498. * Closes all node watchings.
  499. * @param {() => void} onClosed called when every watching is closed
  500. * @returns {void}
  501. */
  502. const closeWatchings = (onClosed) => {
  503. asyncLib.each(
  504. nodes,
  505. (node, callback) => {
  506. if (node.compiler.watching) {
  507. node.compiler.watching.close(callback);
  508. } else {
  509. callback();
  510. }
  511. },
  512. onClosed
  513. );
  514. };
  515. /**
  516. * Processes the provided node.
  517. * @param {Node} node node
  518. * @param {(Error | null)=} err error
  519. * @param {Stats=} stats result
  520. * @returns {void}
  521. */
  522. const nodeDone = (node, err, stats) => {
  523. if (errored) return;
  524. if (err) {
  525. errored = true;
  526. // During setup later watchings do not exist yet — defer the close
  527. // sweep until the setup loop created them all, or they would leak
  528. if (duringSetup) {
  529. setupError = err;
  530. return;
  531. }
  532. return closeWatchings(() => callback(err));
  533. }
  534. node.result = stats;
  535. running--;
  536. if (node.state === "running") {
  537. node.state = "done";
  538. for (const child of node.children) {
  539. if (child.state === "blocked") queue.enqueue(child);
  540. }
  541. } else if (node.state === "running-outdated") {
  542. node.state = "blocked";
  543. queue.enqueue(node);
  544. }
  545. processQueue();
  546. };
  547. /**
  548. * Node invalid from parent.
  549. * @param {Node} node node
  550. * @returns {void}
  551. */
  552. const nodeInvalidFromParent = (node) => {
  553. if (node.state === "done") {
  554. node.state = "blocked";
  555. } else if (node.state === "running") {
  556. node.state = "running-outdated";
  557. }
  558. for (const child of node.children) {
  559. nodeInvalidFromParent(child);
  560. }
  561. };
  562. /**
  563. * Processes the provided node.
  564. * @param {Node} node node
  565. * @returns {void}
  566. */
  567. const nodeInvalid = (node) => {
  568. if (node.state === "done") {
  569. node.state = "pending";
  570. } else if (node.state === "running") {
  571. node.state = "running-outdated";
  572. }
  573. for (const child of node.children) {
  574. nodeInvalidFromParent(child);
  575. }
  576. };
  577. /**
  578. * Processes the provided node.
  579. * @param {Node} node node
  580. * @returns {void}
  581. */
  582. const nodeChange = (node) => {
  583. nodeInvalid(node);
  584. if (node.state === "pending") {
  585. node.state = "blocked";
  586. }
  587. if (node.state === "blocked") {
  588. queue.enqueue(node);
  589. processQueue();
  590. }
  591. };
  592. /** @type {SetupResult[]} */
  593. const setupResults = [];
  594. for (const [i, node] of nodes.entries()) {
  595. setupResults.push(
  596. (node.setupResult = setup(
  597. node.compiler,
  598. i,
  599. nodeDone.bind(null, node),
  600. () => node.state !== "starting" && node.state !== "running",
  601. () => nodeChange(node),
  602. () => nodeInvalid(node)
  603. ))
  604. );
  605. }
  606. let processing = true;
  607. const processQueue = () => {
  608. if (processing) return;
  609. processing = true;
  610. process.nextTick(processQueueWorker);
  611. };
  612. const processQueueWorker = () => {
  613. // eslint-disable-next-line no-unmodified-loop-condition
  614. while (running < parallelism && queue.length > 0 && !errored) {
  615. const node = /** @type {Node} */ (queue.dequeue());
  616. if (
  617. node.state === "queued" ||
  618. (node.state === "blocked" &&
  619. node.parents.every((p) => p.state === "done"))
  620. ) {
  621. running++;
  622. node.state = "starting";
  623. run(
  624. node.compiler,
  625. /** @type {SetupResult} */ (node.setupResult),
  626. nodeDone.bind(null, node)
  627. );
  628. node.state = "running";
  629. }
  630. }
  631. processing = false;
  632. if (
  633. !errored &&
  634. running === 0 &&
  635. nodes.every((node) => node.state === "done")
  636. ) {
  637. /** @type {Stats[]} */
  638. const stats = [];
  639. for (const node of nodes) {
  640. const result = node.result;
  641. if (result) {
  642. node.result = undefined;
  643. stats.push(result);
  644. }
  645. }
  646. if (stats.length > 0) {
  647. callback(null, new MultiStats(stats));
  648. }
  649. }
  650. };
  651. duringSetup = false;
  652. if (setupError) {
  653. // The setup loop finished, every watching exists now — safe to sweep
  654. const err = setupError;
  655. closeWatchings(() => callback(err));
  656. } else {
  657. processQueueWorker();
  658. }
  659. return setupResults;
  660. }
  661. /**
  662. * Returns a compiler watcher.
  663. * @param {WatchOptions | WatchOptions[]} watchOptions the watcher's options
  664. * @param {Callback<MultiStats>} handler signals when the call finishes
  665. * @returns {MultiWatching | undefined} a compiler watcher
  666. */
  667. watch(watchOptions, handler) {
  668. if (this.running) {
  669. handler(new (getConcurrentCompilationError())());
  670. return;
  671. }
  672. // Only mark as running after validation, so a retry reports the real error
  673. if (!this.validateDependencies(handler)) {
  674. this.watching = new MultiWatching([], this);
  675. return this.watching;
  676. }
  677. this.running = true;
  678. const watchings = this._runGraph(
  679. (compiler, idx, callback, isBlocked, setChanged, setInvalid) => {
  680. const watching = compiler.watch(
  681. Array.isArray(watchOptions) ? watchOptions[idx] : watchOptions,
  682. callback
  683. );
  684. if (watching) {
  685. watching._onInvalid = setInvalid;
  686. watching._onChange = setChanged;
  687. watching._isBlocked = isBlocked;
  688. }
  689. return watching;
  690. },
  691. (compiler, watching, _callback) => {
  692. if (compiler.watching !== watching) return;
  693. if (!watching.running) watching.invalidate();
  694. },
  695. (err, stats) => {
  696. // A fatal error tears the whole graph down — allow watching again
  697. if (err) this.running = false;
  698. handler(err, stats);
  699. }
  700. );
  701. this.watching = new MultiWatching(watchings, this);
  702. return this.watching;
  703. }
  704. /**
  705. * Processes the provided multi stat.
  706. * @param {Callback<MultiStats>} callback signals when the call finishes
  707. * @returns {void}
  708. */
  709. run(callback) {
  710. if (this.running) {
  711. callback(new (getConcurrentCompilationError())());
  712. return;
  713. }
  714. // Only mark as running after validation, so a retry reports the real error
  715. if (!this.validateDependencies(callback)) return;
  716. this.running = true;
  717. this._runGraph(
  718. () => {},
  719. (compiler, setupResult, callback) => compiler.run(callback),
  720. (err, stats) => {
  721. this.running = false;
  722. if (callback !== undefined) {
  723. return callback(err, stats);
  724. }
  725. }
  726. );
  727. }
  728. purgeInputFileSystem() {
  729. for (const compiler of this.compilers) {
  730. if (compiler.inputFileSystem && compiler.inputFileSystem.purge) {
  731. compiler.inputFileSystem.purge();
  732. }
  733. }
  734. }
  735. /**
  736. * Processes the provided error callback.
  737. * @param {ErrorCallback} callback signals when the compiler closes
  738. * @returns {void}
  739. */
  740. close(callback) {
  741. if (this.watching) {
  742. // When there is still an active watching, close this first
  743. this.watching.close((_err) => {
  744. this.close(callback);
  745. });
  746. return;
  747. }
  748. asyncLib.each(
  749. this.compilers,
  750. (compiler, callback) => {
  751. compiler.close(callback);
  752. },
  753. (error) => {
  754. callback(/** @type {Error | null} */ (error));
  755. }
  756. );
  757. }
  758. };