UNPKG

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