UNPKG

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