MultiCompiler.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661
  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 { SyncHook, MultiHook } = require("tapable");
  8. const ConcurrentCompilationError = require("./ConcurrentCompilationError");
  9. const MultiStats = require("./MultiStats");
  10. const MultiWatching = require("./MultiWatching");
  11. const WebpackError = require("./WebpackError");
  12. const ArrayQueue = require("./util/ArrayQueue");
  13. /** @template T @typedef {import("tapable").AsyncSeriesHook<T>} AsyncSeriesHook<T> */
  14. /** @template T @template R @typedef {import("tapable").SyncBailHook<T, R>} SyncBailHook<T, R> */
  15. /** @typedef {import("../declarations/WebpackOptions").WatchOptions} WatchOptions */
  16. /** @typedef {import("./Compiler")} Compiler */
  17. /** @typedef {import("./Stats")} Stats */
  18. /** @typedef {import("./Watching")} Watching */
  19. /** @typedef {import("./logging/Logger").Logger} Logger */
  20. /** @typedef {import("./util/fs").InputFileSystem} InputFileSystem */
  21. /** @typedef {import("./util/fs").IntermediateFileSystem} IntermediateFileSystem */
  22. /** @typedef {import("./util/fs").OutputFileSystem} OutputFileSystem */
  23. /** @typedef {import("./util/fs").WatchFileSystem} WatchFileSystem */
  24. /**
  25. * @template T
  26. * @callback Callback
  27. * @param {Error | null} err
  28. * @param {T=} result
  29. */
  30. /**
  31. * @callback RunWithDependenciesHandler
  32. * @param {Compiler} compiler
  33. * @param {Callback<MultiStats>} callback
  34. */
  35. /**
  36. * @typedef {object} MultiCompilerOptions
  37. * @property {number=} parallelism how many Compilers are allows to run at the same time in parallel
  38. */
  39. module.exports = class MultiCompiler {
  40. /**
  41. * @param {Compiler[] | Record<string, Compiler>} compilers child compilers
  42. * @param {MultiCompilerOptions} options options
  43. */
  44. constructor(compilers, options) {
  45. if (!Array.isArray(compilers)) {
  46. /** @type {Compiler[]} */
  47. compilers = Object.keys(compilers).map(name => {
  48. /** @type {Record<string, Compiler>} */
  49. (compilers)[name].name = name;
  50. return /** @type {Record<string, Compiler>} */ (compilers)[name];
  51. });
  52. }
  53. this.hooks = Object.freeze({
  54. /** @type {SyncHook<[MultiStats]>} */
  55. done: new SyncHook(["stats"]),
  56. /** @type {MultiHook<SyncHook<[string | null, number]>>} */
  57. invalid: new MultiHook(compilers.map(c => c.hooks.invalid)),
  58. /** @type {MultiHook<AsyncSeriesHook<[Compiler]>>} */
  59. run: new MultiHook(compilers.map(c => c.hooks.run)),
  60. /** @type {SyncHook<[]>} */
  61. watchClose: new SyncHook([]),
  62. /** @type {MultiHook<AsyncSeriesHook<[Compiler]>>} */
  63. watchRun: new MultiHook(compilers.map(c => c.hooks.watchRun)),
  64. /** @type {MultiHook<SyncBailHook<[string, string, any[]], true>>} */
  65. infrastructureLog: new MultiHook(
  66. compilers.map(c => c.hooks.infrastructureLog)
  67. )
  68. });
  69. this.compilers = compilers;
  70. /** @type {MultiCompilerOptions} */
  71. this._options = {
  72. parallelism: options.parallelism || Infinity
  73. };
  74. /** @type {WeakMap<Compiler, string[]>} */
  75. this.dependencies = new WeakMap();
  76. this.running = false;
  77. /** @type {(Stats | null)[]} */
  78. const compilerStats = this.compilers.map(() => null);
  79. let doneCompilers = 0;
  80. for (let index = 0; index < this.compilers.length; index++) {
  81. const compiler = this.compilers[index];
  82. const compilerIndex = index;
  83. let compilerDone = false;
  84. compiler.hooks.done.tap("MultiCompiler", stats => {
  85. if (!compilerDone) {
  86. compilerDone = true;
  87. doneCompilers++;
  88. }
  89. compilerStats[compilerIndex] = stats;
  90. if (doneCompilers === this.compilers.length) {
  91. this.hooks.done.call(
  92. new MultiStats(/** @type {Stats[]} */ (compilerStats))
  93. );
  94. }
  95. });
  96. compiler.hooks.invalid.tap("MultiCompiler", () => {
  97. if (compilerDone) {
  98. compilerDone = false;
  99. doneCompilers--;
  100. }
  101. });
  102. }
  103. this._validateCompilersOptions();
  104. }
  105. _validateCompilersOptions() {
  106. if (this.compilers.length < 2) return;
  107. /**
  108. * @param {Compiler} compiler compiler
  109. * @param {WebpackError} warning warning
  110. */
  111. const addWarning = (compiler, warning) => {
  112. compiler.hooks.thisCompilation.tap("MultiCompiler", compilation => {
  113. compilation.warnings.push(warning);
  114. });
  115. };
  116. const cacheNames = new Set();
  117. for (const compiler of this.compilers) {
  118. if (compiler.options.cache && "name" in compiler.options.cache) {
  119. const name = compiler.options.cache.name;
  120. if (cacheNames.has(name)) {
  121. addWarning(
  122. compiler,
  123. new WebpackError(
  124. `${
  125. compiler.name
  126. ? `Compiler with name "${compiler.name}" doesn't use unique cache name. `
  127. : ""
  128. }Please set unique "cache.name" option. Name "${name}" already used.`
  129. )
  130. );
  131. } else {
  132. cacheNames.add(name);
  133. }
  134. }
  135. }
  136. }
  137. get options() {
  138. return Object.assign(
  139. this.compilers.map(c => c.options),
  140. this._options
  141. );
  142. }
  143. get outputPath() {
  144. let commonPath = this.compilers[0].outputPath;
  145. for (const compiler of this.compilers) {
  146. while (
  147. compiler.outputPath.indexOf(commonPath) !== 0 &&
  148. /[/\\]/.test(commonPath)
  149. ) {
  150. commonPath = commonPath.replace(/[/\\][^/\\]*$/, "");
  151. }
  152. }
  153. if (!commonPath && this.compilers[0].outputPath[0] === "/") return "/";
  154. return commonPath;
  155. }
  156. get inputFileSystem() {
  157. throw new Error("Cannot read inputFileSystem of a MultiCompiler");
  158. }
  159. get outputFileSystem() {
  160. throw new Error("Cannot read outputFileSystem of a MultiCompiler");
  161. }
  162. get watchFileSystem() {
  163. throw new Error("Cannot read watchFileSystem of a MultiCompiler");
  164. }
  165. get intermediateFileSystem() {
  166. throw new Error("Cannot read outputFileSystem of a MultiCompiler");
  167. }
  168. /**
  169. * @param {InputFileSystem} value the new input file system
  170. */
  171. set inputFileSystem(value) {
  172. for (const compiler of this.compilers) {
  173. compiler.inputFileSystem = value;
  174. }
  175. }
  176. /**
  177. * @param {OutputFileSystem} value the new output file system
  178. */
  179. set outputFileSystem(value) {
  180. for (const compiler of this.compilers) {
  181. compiler.outputFileSystem = value;
  182. }
  183. }
  184. /**
  185. * @param {WatchFileSystem} value the new watch file system
  186. */
  187. set watchFileSystem(value) {
  188. for (const compiler of this.compilers) {
  189. compiler.watchFileSystem = value;
  190. }
  191. }
  192. /**
  193. * @param {IntermediateFileSystem} value the new intermediate file system
  194. */
  195. set intermediateFileSystem(value) {
  196. for (const compiler of this.compilers) {
  197. compiler.intermediateFileSystem = value;
  198. }
  199. }
  200. /**
  201. * @param {string | (function(): string)} name name of the logger, or function called once to get the logger name
  202. * @returns {Logger} a logger with that name
  203. */
  204. getInfrastructureLogger(name) {
  205. return this.compilers[0].getInfrastructureLogger(name);
  206. }
  207. /**
  208. * @param {Compiler} compiler the child compiler
  209. * @param {string[]} dependencies its dependencies
  210. * @returns {void}
  211. */
  212. setDependencies(compiler, dependencies) {
  213. this.dependencies.set(compiler, dependencies);
  214. }
  215. /**
  216. * @param {Callback<MultiStats>} callback signals when the validation is complete
  217. * @returns {boolean} true if the dependencies are valid
  218. */
  219. validateDependencies(callback) {
  220. /** @type {Set<{source: Compiler, target: Compiler}>} */
  221. const edges = new Set();
  222. /** @type {string[]} */
  223. const missing = [];
  224. /**
  225. * @param {Compiler} compiler compiler
  226. * @returns {boolean} target was found
  227. */
  228. const targetFound = compiler => {
  229. for (const edge of edges) {
  230. if (edge.target === compiler) {
  231. return true;
  232. }
  233. }
  234. return false;
  235. };
  236. /**
  237. * @param {{source: Compiler, target: Compiler}} e1 edge 1
  238. * @param {{source: Compiler, target: Compiler}} e2 edge 2
  239. * @returns {number} result
  240. */
  241. const sortEdges = (e1, e2) => {
  242. return (
  243. /** @type {string} */
  244. (e1.source.name).localeCompare(
  245. /** @type {string} */ (e2.source.name)
  246. ) ||
  247. /** @type {string} */
  248. (e1.target.name).localeCompare(/** @type {string} */ (e2.target.name))
  249. );
  250. };
  251. for (const source of this.compilers) {
  252. const dependencies = this.dependencies.get(source);
  253. if (dependencies) {
  254. for (const dep of dependencies) {
  255. const target = this.compilers.find(c => c.name === dep);
  256. if (!target) {
  257. missing.push(dep);
  258. } else {
  259. edges.add({
  260. source,
  261. target
  262. });
  263. }
  264. }
  265. }
  266. }
  267. /** @type {string[]} */
  268. const errors = missing.map(m => `Compiler dependency \`${m}\` not found.`);
  269. const stack = this.compilers.filter(c => !targetFound(c));
  270. while (stack.length > 0) {
  271. const current = stack.pop();
  272. for (const edge of edges) {
  273. if (edge.source === current) {
  274. edges.delete(edge);
  275. const target = edge.target;
  276. if (!targetFound(target)) {
  277. stack.push(target);
  278. }
  279. }
  280. }
  281. }
  282. if (edges.size > 0) {
  283. /** @type {string[]} */
  284. const lines = Array.from(edges)
  285. .sort(sortEdges)
  286. .map(edge => `${edge.source.name} -> ${edge.target.name}`);
  287. lines.unshift("Circular dependency found in compiler dependencies.");
  288. errors.unshift(lines.join("\n"));
  289. }
  290. if (errors.length > 0) {
  291. const message = errors.join("\n");
  292. callback(new Error(message));
  293. return false;
  294. }
  295. return true;
  296. }
  297. // TODO webpack 6 remove
  298. /**
  299. * @deprecated This method should have been private
  300. * @param {Compiler[]} compilers the child compilers
  301. * @param {RunWithDependenciesHandler} fn a handler to run for each compiler
  302. * @param {Callback<MultiStats>} callback the compiler's handler
  303. * @returns {void}
  304. */
  305. runWithDependencies(compilers, fn, callback) {
  306. const fulfilledNames = new Set();
  307. let remainingCompilers = compilers;
  308. /**
  309. * @param {string} d dependency
  310. * @returns {boolean} when dependency was fulfilled
  311. */
  312. const isDependencyFulfilled = d => fulfilledNames.has(d);
  313. /**
  314. * @returns {Compiler[]} compilers
  315. */
  316. const getReadyCompilers = () => {
  317. let readyCompilers = [];
  318. let list = remainingCompilers;
  319. remainingCompilers = [];
  320. for (const c of list) {
  321. const dependencies = this.dependencies.get(c);
  322. const ready =
  323. !dependencies || dependencies.every(isDependencyFulfilled);
  324. if (ready) {
  325. readyCompilers.push(c);
  326. } else {
  327. remainingCompilers.push(c);
  328. }
  329. }
  330. return readyCompilers;
  331. };
  332. /**
  333. * @param {Callback<MultiStats>} callback callback
  334. * @returns {void}
  335. */
  336. const runCompilers = callback => {
  337. if (remainingCompilers.length === 0) return callback(null);
  338. asyncLib.map(
  339. getReadyCompilers(),
  340. (compiler, callback) => {
  341. fn(compiler, err => {
  342. if (err) return callback(err);
  343. fulfilledNames.add(compiler.name);
  344. runCompilers(callback);
  345. });
  346. },
  347. (err, results) => {
  348. callback(err, /** @type {TODO} */ (results));
  349. }
  350. );
  351. };
  352. runCompilers(callback);
  353. }
  354. /**
  355. * @template SetupResult
  356. * @param {function(Compiler, number, Callback<Stats>, function(): boolean, function(): void, function(): void): SetupResult} setup setup a single compiler
  357. * @param {function(Compiler, SetupResult, Callback<Stats>): void} run run/continue a single compiler
  358. * @param {Callback<MultiStats>} callback callback when all compilers are done, result includes Stats of all changed compilers
  359. * @returns {SetupResult[]} result of setup
  360. */
  361. _runGraph(setup, run, callback) {
  362. /** @typedef {{ compiler: Compiler, setupResult: undefined | SetupResult, result: undefined | Stats, state: "pending" | "blocked" | "queued" | "starting" | "running" | "running-outdated" | "done", children: Node[], parents: Node[] }} Node */
  363. // State transitions for nodes:
  364. // -> blocked (initial)
  365. // blocked -> starting [running++] (when all parents done)
  366. // queued -> starting [running++] (when processing the queue)
  367. // starting -> running (when run has been called)
  368. // running -> done [running--] (when compilation is done)
  369. // done -> pending (when invalidated from file change)
  370. // pending -> blocked [add to queue] (when invalidated from aggregated changes)
  371. // done -> blocked [add to queue] (when invalidated, from parent invalidation)
  372. // running -> running-outdated (when invalidated, either from change or parent invalidation)
  373. // running-outdated -> blocked [running--] (when compilation is done)
  374. /** @type {Node[]} */
  375. const nodes = this.compilers.map(compiler => ({
  376. compiler,
  377. setupResult: undefined,
  378. result: undefined,
  379. state: "blocked",
  380. children: [],
  381. parents: []
  382. }));
  383. /** @type {Map<string, Node>} */
  384. const compilerToNode = new Map();
  385. for (const node of nodes) {
  386. compilerToNode.set(/** @type {string} */ (node.compiler.name), node);
  387. }
  388. for (const node of nodes) {
  389. const dependencies = this.dependencies.get(node.compiler);
  390. if (!dependencies) continue;
  391. for (const dep of dependencies) {
  392. const parent = /** @type {Node} */ (compilerToNode.get(dep));
  393. node.parents.push(parent);
  394. parent.children.push(node);
  395. }
  396. }
  397. /** @type {ArrayQueue<Node>} */
  398. const queue = new ArrayQueue();
  399. for (const node of nodes) {
  400. if (node.parents.length === 0) {
  401. node.state = "queued";
  402. queue.enqueue(node);
  403. }
  404. }
  405. let errored = false;
  406. let running = 0;
  407. const parallelism = /** @type {number} */ (this._options.parallelism);
  408. /**
  409. * @param {Node} node node
  410. * @param {(Error | null)=} err error
  411. * @param {Stats=} stats result
  412. * @returns {void}
  413. */
  414. const nodeDone = (node, err, stats) => {
  415. if (errored) return;
  416. if (err) {
  417. errored = true;
  418. return asyncLib.each(
  419. nodes,
  420. (node, callback) => {
  421. if (node.compiler.watching) {
  422. node.compiler.watching.close(callback);
  423. } else {
  424. callback();
  425. }
  426. },
  427. () => callback(err)
  428. );
  429. }
  430. node.result = stats;
  431. running--;
  432. if (node.state === "running") {
  433. node.state = "done";
  434. for (const child of node.children) {
  435. if (child.state === "blocked") queue.enqueue(child);
  436. }
  437. } else if (node.state === "running-outdated") {
  438. node.state = "blocked";
  439. queue.enqueue(node);
  440. }
  441. processQueue();
  442. };
  443. /**
  444. * @param {Node} node node
  445. * @returns {void}
  446. */
  447. const nodeInvalidFromParent = node => {
  448. if (node.state === "done") {
  449. node.state = "blocked";
  450. } else if (node.state === "running") {
  451. node.state = "running-outdated";
  452. }
  453. for (const child of node.children) {
  454. nodeInvalidFromParent(child);
  455. }
  456. };
  457. /**
  458. * @param {Node} node node
  459. * @returns {void}
  460. */
  461. const nodeInvalid = node => {
  462. if (node.state === "done") {
  463. node.state = "pending";
  464. } else if (node.state === "running") {
  465. node.state = "running-outdated";
  466. }
  467. for (const child of node.children) {
  468. nodeInvalidFromParent(child);
  469. }
  470. };
  471. /**
  472. * @param {Node} node node
  473. * @returns {void}
  474. */
  475. const nodeChange = node => {
  476. nodeInvalid(node);
  477. if (node.state === "pending") {
  478. node.state = "blocked";
  479. }
  480. if (node.state === "blocked") {
  481. queue.enqueue(node);
  482. processQueue();
  483. }
  484. };
  485. /** @type {SetupResult[]} */
  486. const setupResults = [];
  487. nodes.forEach((node, i) => {
  488. setupResults.push(
  489. (node.setupResult = setup(
  490. node.compiler,
  491. i,
  492. nodeDone.bind(null, node),
  493. () => node.state !== "starting" && node.state !== "running",
  494. () => nodeChange(node),
  495. () => nodeInvalid(node)
  496. ))
  497. );
  498. });
  499. let processing = true;
  500. const processQueue = () => {
  501. if (processing) return;
  502. processing = true;
  503. process.nextTick(processQueueWorker);
  504. };
  505. const processQueueWorker = () => {
  506. while (running < parallelism && queue.length > 0 && !errored) {
  507. const node = /** @type {Node} */ (queue.dequeue());
  508. if (
  509. node.state === "queued" ||
  510. (node.state === "blocked" &&
  511. node.parents.every(p => p.state === "done"))
  512. ) {
  513. running++;
  514. node.state = "starting";
  515. run(
  516. node.compiler,
  517. /** @type {SetupResult} */ (node.setupResult),
  518. nodeDone.bind(null, node)
  519. );
  520. node.state = "running";
  521. }
  522. }
  523. processing = false;
  524. if (
  525. !errored &&
  526. running === 0 &&
  527. nodes.every(node => node.state === "done")
  528. ) {
  529. const stats = [];
  530. for (const node of nodes) {
  531. const result = node.result;
  532. if (result) {
  533. node.result = undefined;
  534. stats.push(result);
  535. }
  536. }
  537. if (stats.length > 0) {
  538. callback(null, new MultiStats(stats));
  539. }
  540. }
  541. };
  542. processQueueWorker();
  543. return setupResults;
  544. }
  545. /**
  546. * @param {WatchOptions|WatchOptions[]} watchOptions the watcher's options
  547. * @param {Callback<MultiStats>} handler signals when the call finishes
  548. * @returns {MultiWatching} a compiler watcher
  549. */
  550. watch(watchOptions, handler) {
  551. if (this.running) {
  552. return handler(new ConcurrentCompilationError());
  553. }
  554. this.running = true;
  555. if (this.validateDependencies(handler)) {
  556. const watchings = this._runGraph(
  557. (compiler, idx, callback, isBlocked, setChanged, setInvalid) => {
  558. const watching = compiler.watch(
  559. Array.isArray(watchOptions) ? watchOptions[idx] : watchOptions,
  560. callback
  561. );
  562. if (watching) {
  563. watching._onInvalid = setInvalid;
  564. watching._onChange = setChanged;
  565. watching._isBlocked = isBlocked;
  566. }
  567. return watching;
  568. },
  569. (compiler, watching, callback) => {
  570. if (compiler.watching !== watching) return;
  571. if (!watching.running) watching.invalidate();
  572. },
  573. handler
  574. );
  575. return new MultiWatching(watchings, this);
  576. }
  577. return new MultiWatching([], this);
  578. }
  579. /**
  580. * @param {Callback<MultiStats>} callback signals when the call finishes
  581. * @returns {void}
  582. */
  583. run(callback) {
  584. if (this.running) {
  585. return callback(new ConcurrentCompilationError());
  586. }
  587. this.running = true;
  588. if (this.validateDependencies(callback)) {
  589. this._runGraph(
  590. () => {},
  591. (compiler, setupResult, callback) => compiler.run(callback),
  592. (err, stats) => {
  593. this.running = false;
  594. if (callback !== undefined) {
  595. return callback(err, stats);
  596. }
  597. }
  598. );
  599. }
  600. }
  601. purgeInputFileSystem() {
  602. for (const compiler of this.compilers) {
  603. if (compiler.inputFileSystem && compiler.inputFileSystem.purge) {
  604. compiler.inputFileSystem.purge();
  605. }
  606. }
  607. }
  608. /**
  609. * @param {Callback<void>} callback signals when the compiler closes
  610. * @returns {void}
  611. */
  612. close(callback) {
  613. asyncLib.each(
  614. this.compilers,
  615. (compiler, callback) => {
  616. compiler.close(callback);
  617. },
  618. error => {
  619. callback(error);
  620. }
  621. );
  622. }
  623. };