You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

290 lines
7.2 KiB

4 years ago
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const { Tapable, SyncHook, MultiHook } = require("tapable");
  7. const asyncLib = require("neo-async");
  8. const MultiWatching = require("./MultiWatching");
  9. const MultiStats = require("./MultiStats");
  10. const ConcurrentCompilationError = require("./ConcurrentCompilationError");
  11. module.exports = class MultiCompiler extends Tapable {
  12. constructor(compilers) {
  13. super();
  14. this.hooks = {
  15. done: new SyncHook(["stats"]),
  16. invalid: new MultiHook(compilers.map(c => c.hooks.invalid)),
  17. run: new MultiHook(compilers.map(c => c.hooks.run)),
  18. watchClose: new SyncHook([]),
  19. watchRun: new MultiHook(compilers.map(c => c.hooks.watchRun)),
  20. infrastructureLog: new MultiHook(
  21. compilers.map(c => c.hooks.infrastructureLog)
  22. )
  23. };
  24. if (!Array.isArray(compilers)) {
  25. compilers = Object.keys(compilers).map(name => {
  26. compilers[name].name = name;
  27. return compilers[name];
  28. });
  29. }
  30. this.compilers = compilers;
  31. let doneCompilers = 0;
  32. let compilerStats = [];
  33. let index = 0;
  34. for (const compiler of this.compilers) {
  35. let compilerDone = false;
  36. const compilerIndex = index++;
  37. // eslint-disable-next-line no-loop-func
  38. compiler.hooks.done.tap("MultiCompiler", stats => {
  39. if (!compilerDone) {
  40. compilerDone = true;
  41. doneCompilers++;
  42. }
  43. compilerStats[compilerIndex] = stats;
  44. if (doneCompilers === this.compilers.length) {
  45. this.hooks.done.call(new MultiStats(compilerStats));
  46. }
  47. });
  48. // eslint-disable-next-line no-loop-func
  49. compiler.hooks.invalid.tap("MultiCompiler", () => {
  50. if (compilerDone) {
  51. compilerDone = false;
  52. doneCompilers--;
  53. }
  54. });
  55. }
  56. this.running = false;
  57. }
  58. get outputPath() {
  59. let commonPath = this.compilers[0].outputPath;
  60. for (const compiler of this.compilers) {
  61. while (
  62. compiler.outputPath.indexOf(commonPath) !== 0 &&
  63. /[/\\]/.test(commonPath)
  64. ) {
  65. commonPath = commonPath.replace(/[/\\][^/\\]*$/, "");
  66. }
  67. }
  68. if (!commonPath && this.compilers[0].outputPath[0] === "/") return "/";
  69. return commonPath;
  70. }
  71. get inputFileSystem() {
  72. throw new Error("Cannot read inputFileSystem of a MultiCompiler");
  73. }
  74. get outputFileSystem() {
  75. throw new Error("Cannot read outputFileSystem of a MultiCompiler");
  76. }
  77. set inputFileSystem(value) {
  78. for (const compiler of this.compilers) {
  79. compiler.inputFileSystem = value;
  80. }
  81. }
  82. set outputFileSystem(value) {
  83. for (const compiler of this.compilers) {
  84. compiler.outputFileSystem = value;
  85. }
  86. }
  87. getInfrastructureLogger(name) {
  88. return this.compilers[0].getInfrastructureLogger(name);
  89. }
  90. validateDependencies(callback) {
  91. const edges = new Set();
  92. const missing = [];
  93. const targetFound = compiler => {
  94. for (const edge of edges) {
  95. if (edge.target === compiler) {
  96. return true;
  97. }
  98. }
  99. return false;
  100. };
  101. const sortEdges = (e1, e2) => {
  102. return (
  103. e1.source.name.localeCompare(e2.source.name) ||
  104. e1.target.name.localeCompare(e2.target.name)
  105. );
  106. };
  107. for (const source of this.compilers) {
  108. if (source.dependencies) {
  109. for (const dep of source.dependencies) {
  110. const target = this.compilers.find(c => c.name === dep);
  111. if (!target) {
  112. missing.push(dep);
  113. } else {
  114. edges.add({
  115. source,
  116. target
  117. });
  118. }
  119. }
  120. }
  121. }
  122. const errors = missing.map(m => `Compiler dependency \`${m}\` not found.`);
  123. const stack = this.compilers.filter(c => !targetFound(c));
  124. while (stack.length > 0) {
  125. const current = stack.pop();
  126. for (const edge of edges) {
  127. if (edge.source === current) {
  128. edges.delete(edge);
  129. const target = edge.target;
  130. if (!targetFound(target)) {
  131. stack.push(target);
  132. }
  133. }
  134. }
  135. }
  136. if (edges.size > 0) {
  137. const lines = Array.from(edges)
  138. .sort(sortEdges)
  139. .map(edge => `${edge.source.name} -> ${edge.target.name}`);
  140. lines.unshift("Circular dependency found in compiler dependencies.");
  141. errors.unshift(lines.join("\n"));
  142. }
  143. if (errors.length > 0) {
  144. const message = errors.join("\n");
  145. callback(new Error(message));
  146. return false;
  147. }
  148. return true;
  149. }
  150. runWithDependencies(compilers, fn, callback) {
  151. const fulfilledNames = new Set();
  152. let remainingCompilers = compilers;
  153. const isDependencyFulfilled = d => fulfilledNames.has(d);
  154. const getReadyCompilers = () => {
  155. let readyCompilers = [];
  156. let list = remainingCompilers;
  157. remainingCompilers = [];
  158. for (const c of list) {
  159. const ready =
  160. !c.dependencies || c.dependencies.every(isDependencyFulfilled);
  161. if (ready) {
  162. readyCompilers.push(c);
  163. } else {
  164. remainingCompilers.push(c);
  165. }
  166. }
  167. return readyCompilers;
  168. };
  169. const runCompilers = callback => {
  170. if (remainingCompilers.length === 0) return callback();
  171. asyncLib.map(
  172. getReadyCompilers(),
  173. (compiler, callback) => {
  174. fn(compiler, err => {
  175. if (err) return callback(err);
  176. fulfilledNames.add(compiler.name);
  177. runCompilers(callback);
  178. });
  179. },
  180. callback
  181. );
  182. };
  183. runCompilers(callback);
  184. }
  185. watch(watchOptions, handler) {
  186. if (this.running) return handler(new ConcurrentCompilationError());
  187. let watchings = [];
  188. let allStats = this.compilers.map(() => null);
  189. let compilerStatus = this.compilers.map(() => false);
  190. if (this.validateDependencies(handler)) {
  191. this.running = true;
  192. this.runWithDependencies(
  193. this.compilers,
  194. (compiler, callback) => {
  195. const compilerIdx = this.compilers.indexOf(compiler);
  196. let firstRun = true;
  197. let watching = compiler.watch(
  198. Array.isArray(watchOptions)
  199. ? watchOptions[compilerIdx]
  200. : watchOptions,
  201. (err, stats) => {
  202. if (err) handler(err);
  203. if (stats) {
  204. allStats[compilerIdx] = stats;
  205. compilerStatus[compilerIdx] = "new";
  206. if (compilerStatus.every(Boolean)) {
  207. const freshStats = allStats.filter((s, idx) => {
  208. return compilerStatus[idx] === "new";
  209. });
  210. compilerStatus.fill(true);
  211. const multiStats = new MultiStats(freshStats);
  212. handler(null, multiStats);
  213. }
  214. }
  215. if (firstRun && !err) {
  216. firstRun = false;
  217. callback();
  218. }
  219. }
  220. );
  221. watchings.push(watching);
  222. },
  223. () => {
  224. // ignore
  225. }
  226. );
  227. }
  228. return new MultiWatching(watchings, this);
  229. }
  230. run(callback) {
  231. if (this.running) {
  232. return callback(new ConcurrentCompilationError());
  233. }
  234. const finalCallback = (err, stats) => {
  235. this.running = false;
  236. if (callback !== undefined) {
  237. return callback(err, stats);
  238. }
  239. };
  240. const allStats = this.compilers.map(() => null);
  241. if (this.validateDependencies(callback)) {
  242. this.running = true;
  243. this.runWithDependencies(
  244. this.compilers,
  245. (compiler, callback) => {
  246. const compilerIdx = this.compilers.indexOf(compiler);
  247. compiler.run((err, stats) => {
  248. if (err) {
  249. return callback(err);
  250. }
  251. allStats[compilerIdx] = stats;
  252. callback();
  253. });
  254. },
  255. err => {
  256. if (err) {
  257. return finalCallback(err);
  258. }
  259. finalCallback(null, new MultiStats(allStats));
  260. }
  261. );
  262. }
  263. }
  264. purgeInputFileSystem() {
  265. for (const compiler of this.compilers) {
  266. if (compiler.inputFileSystem && compiler.inputFileSystem.purge) {
  267. compiler.inputFileSystem.purge();
  268. }
  269. }
  270. }
  271. };