UNPKG

11.3 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5
6"use strict";
7
8const asyncLib = require("neo-async");
9const { SyncHook, MultiHook } = require("tapable");
10
11const ConcurrentCompilationError = require("./ConcurrentCompilationError");
12const MultiStats = require("./MultiStats");
13const MultiWatching = require("./MultiWatching");
14
15/** @template T @typedef {import("tapable").AsyncSeriesHook<T>} AsyncSeriesHook<T> */
16/** @template T @template R @typedef {import("tapable").SyncBailHook<T, R>} SyncBailHook<T, R> */
17/** @typedef {import("../declarations/WebpackOptions").WatchOptions} WatchOptions */
18/** @typedef {import("./Compiler")} Compiler */
19/** @typedef {import("./Stats")} Stats */
20/** @typedef {import("./Watching")} Watching */
21/** @typedef {import("./util/fs").InputFileSystem} InputFileSystem */
22/** @typedef {import("./util/fs").IntermediateFileSystem} IntermediateFileSystem */
23/** @typedef {import("./util/fs").OutputFileSystem} OutputFileSystem */
24/** @typedef {import("./util/fs").WatchFileSystem} WatchFileSystem */
25
26/** @typedef {number} CompilerStatus */
27
28const STATUS_PENDING = 0;
29const STATUS_DONE = 1;
30const STATUS_NEW = 2;
31
32/**
33 * @template T
34 * @callback Callback
35 * @param {Error=} err
36 * @param {T=} result
37 */
38
39/**
40 * @callback RunWithDependenciesHandler
41 * @param {Compiler} compiler
42 * @param {Callback<MultiStats>} callback
43 */
44
45module.exports = class MultiCompiler {
46 /**
47 * @param {Compiler[] | Record<string, Compiler>} compilers child compilers
48 */
49 constructor(compilers) {
50 if (!Array.isArray(compilers)) {
51 compilers = Object.keys(compilers).map(name => {
52 compilers[name].name = name;
53 return compilers[name];
54 });
55 }
56
57 this.hooks = Object.freeze({
58 /** @type {SyncHook<[MultiStats]>} */
59 done: new SyncHook(["stats"]),
60 /** @type {MultiHook<SyncHook<[string | null, number]>>} */
61 invalid: new MultiHook(compilers.map(c => c.hooks.invalid)),
62 /** @type {MultiHook<AsyncSeriesHook<[Compiler]>>} */
63 run: new MultiHook(compilers.map(c => c.hooks.run)),
64 /** @type {SyncHook<[]>} */
65 watchClose: new SyncHook([]),
66 /** @type {MultiHook<AsyncSeriesHook<[Compiler]>>} */
67 watchRun: new MultiHook(compilers.map(c => c.hooks.watchRun)),
68 /** @type {MultiHook<SyncBailHook<[string, string, any[]], true>>} */
69 infrastructureLog: new MultiHook(
70 compilers.map(c => c.hooks.infrastructureLog)
71 )
72 });
73 this.compilers = compilers;
74 /** @type {WeakMap<Compiler, string[]>} */
75 this.dependencies = new WeakMap();
76 this.running = false;
77
78 /** @type {Stats[]} */
79 const compilerStats = this.compilers.map(() => null);
80 let doneCompilers = 0;
81 for (let index = 0; index < this.compilers.length; index++) {
82 const compiler = this.compilers[index];
83 const compilerIndex = index;
84 let compilerDone = false;
85 // eslint-disable-next-line no-loop-func
86 compiler.hooks.done.tap("MultiCompiler", stats => {
87 if (!compilerDone) {
88 compilerDone = true;
89 doneCompilers++;
90 }
91 compilerStats[compilerIndex] = stats;
92 if (doneCompilers === this.compilers.length) {
93 this.hooks.done.call(new MultiStats(compilerStats));
94 }
95 });
96 // eslint-disable-next-line no-loop-func
97 compiler.hooks.invalid.tap("MultiCompiler", () => {
98 if (compilerDone) {
99 compilerDone = false;
100 doneCompilers--;
101 }
102 });
103 }
104 }
105
106 get options() {
107 return this.compilers.map(c => c.options);
108 }
109
110 get outputPath() {
111 let commonPath = this.compilers[0].outputPath;
112 for (const compiler of this.compilers) {
113 while (
114 compiler.outputPath.indexOf(commonPath) !== 0 &&
115 /[/\\]/.test(commonPath)
116 ) {
117 commonPath = commonPath.replace(/[/\\][^/\\]*$/, "");
118 }
119 }
120
121 if (!commonPath && this.compilers[0].outputPath[0] === "/") return "/";
122 return commonPath;
123 }
124
125 get inputFileSystem() {
126 throw new Error("Cannot read inputFileSystem of a MultiCompiler");
127 }
128
129 get outputFileSystem() {
130 throw new Error("Cannot read outputFileSystem of a MultiCompiler");
131 }
132
133 get watchFileSystem() {
134 throw new Error("Cannot read watchFileSystem of a MultiCompiler");
135 }
136
137 get intermediateFileSystem() {
138 throw new Error("Cannot read outputFileSystem of a MultiCompiler");
139 }
140
141 /**
142 * @param {InputFileSystem} value the new input file system
143 */
144 set inputFileSystem(value) {
145 for (const compiler of this.compilers) {
146 compiler.inputFileSystem = value;
147 }
148 }
149
150 /**
151 * @param {OutputFileSystem} value the new output file system
152 */
153 set outputFileSystem(value) {
154 for (const compiler of this.compilers) {
155 compiler.outputFileSystem = value;
156 }
157 }
158
159 /**
160 * @param {WatchFileSystem} value the new watch file system
161 */
162 set watchFileSystem(value) {
163 for (const compiler of this.compilers) {
164 compiler.watchFileSystem = value;
165 }
166 }
167
168 /**
169 * @param {IntermediateFileSystem} value the new intermediate file system
170 */
171 set intermediateFileSystem(value) {
172 for (const compiler of this.compilers) {
173 compiler.intermediateFileSystem = value;
174 }
175 }
176
177 getInfrastructureLogger(name) {
178 return this.compilers[0].getInfrastructureLogger(name);
179 }
180
181 /**
182 * @param {Compiler} compiler the child compiler
183 * @param {string[]} dependencies its dependencies
184 * @returns {void}
185 */
186 setDependencies(compiler, dependencies) {
187 this.dependencies.set(compiler, dependencies);
188 }
189
190 /**
191 * @param {Callback<MultiStats>} callback signals when the validation is complete
192 * @returns {boolean} true if the dependencies are valid
193 */
194 validateDependencies(callback) {
195 /** @type {Set<{source: Compiler, target: Compiler}>} */
196 const edges = new Set();
197 /** @type {string[]} */
198 const missing = [];
199 const targetFound = compiler => {
200 for (const edge of edges) {
201 if (edge.target === compiler) {
202 return true;
203 }
204 }
205 return false;
206 };
207 const sortEdges = (e1, e2) => {
208 return (
209 e1.source.name.localeCompare(e2.source.name) ||
210 e1.target.name.localeCompare(e2.target.name)
211 );
212 };
213 for (const source of this.compilers) {
214 const dependencies = this.dependencies.get(source);
215 if (dependencies) {
216 for (const dep of dependencies) {
217 const target = this.compilers.find(c => c.name === dep);
218 if (!target) {
219 missing.push(dep);
220 } else {
221 edges.add({
222 source,
223 target
224 });
225 }
226 }
227 }
228 }
229 const errors = missing.map(m => `Compiler dependency \`${m}\` not found.`);
230 const stack = this.compilers.filter(c => !targetFound(c));
231 while (stack.length > 0) {
232 const current = stack.pop();
233 for (const edge of edges) {
234 if (edge.source === current) {
235 edges.delete(edge);
236 const target = edge.target;
237 if (!targetFound(target)) {
238 stack.push(target);
239 }
240 }
241 }
242 }
243 if (edges.size > 0) {
244 const lines = Array.from(edges)
245 .sort(sortEdges)
246 .map(edge => `${edge.source.name} -> ${edge.target.name}`);
247 lines.unshift("Circular dependency found in compiler dependencies.");
248 errors.unshift(lines.join("\n"));
249 }
250 if (errors.length > 0) {
251 const message = errors.join("\n");
252 callback(new Error(message));
253 return false;
254 }
255 return true;
256 }
257
258 /**
259 * @param {Compiler[]} compilers the child compilers
260 * @param {RunWithDependenciesHandler} fn a handler to run for each compiler
261 * @param {Callback<MultiStats>} callback the compiler's handler
262 * @returns {void}
263 */
264 runWithDependencies(compilers, fn, callback) {
265 const fulfilledNames = new Set();
266 let remainingCompilers = compilers;
267 const isDependencyFulfilled = d => fulfilledNames.has(d);
268 const getReadyCompilers = () => {
269 let readyCompilers = [];
270 let list = remainingCompilers;
271 remainingCompilers = [];
272 for (const c of list) {
273 const dependencies = this.dependencies.get(c);
274 const ready =
275 !dependencies || dependencies.every(isDependencyFulfilled);
276 if (ready) {
277 readyCompilers.push(c);
278 } else {
279 remainingCompilers.push(c);
280 }
281 }
282 return readyCompilers;
283 };
284 const runCompilers = callback => {
285 if (remainingCompilers.length === 0) return callback();
286 asyncLib.map(
287 getReadyCompilers(),
288 (compiler, callback) => {
289 fn(compiler, err => {
290 if (err) return callback(err);
291 fulfilledNames.add(compiler.name);
292 runCompilers(callback);
293 });
294 },
295 callback
296 );
297 };
298 runCompilers(callback);
299 }
300
301 /**
302 * @param {WatchOptions|WatchOptions[]} watchOptions the watcher's options
303 * @param {Callback<MultiStats>} handler signals when the call finishes
304 * @returns {MultiWatching} a compiler watcher
305 */
306 watch(watchOptions, handler) {
307 if (this.running) {
308 return handler(new ConcurrentCompilationError());
309 }
310
311 /** @type {Watching[]} */
312 const watchings = [];
313
314 /** @type {Stats[]} */
315 const allStats = this.compilers.map(() => null);
316
317 /** @type {CompilerStatus[]} */
318 const compilerStatus = this.compilers.map(() => STATUS_PENDING);
319
320 if (this.validateDependencies(handler)) {
321 this.running = true;
322 this.runWithDependencies(
323 this.compilers,
324 (compiler, callback) => {
325 const compilerIdx = this.compilers.indexOf(compiler);
326 let firstRun = true;
327 let watching = compiler.watch(
328 Array.isArray(watchOptions)
329 ? watchOptions[compilerIdx]
330 : watchOptions,
331 (err, stats) => {
332 if (err) handler(err);
333 if (stats) {
334 allStats[compilerIdx] = stats;
335 compilerStatus[compilerIdx] = STATUS_NEW;
336 if (compilerStatus.every(status => status !== STATUS_PENDING)) {
337 const freshStats = allStats.filter((s, idx) => {
338 return compilerStatus[idx] === STATUS_NEW;
339 });
340 compilerStatus.fill(STATUS_DONE);
341 const multiStats = new MultiStats(freshStats);
342 handler(null, multiStats);
343 }
344 }
345 if (firstRun && !err) {
346 firstRun = false;
347 callback();
348 }
349 }
350 );
351 watchings.push(watching);
352 },
353 () => {
354 // ignore
355 }
356 );
357 }
358
359 return new MultiWatching(watchings, this);
360 }
361
362 /**
363 * @param {Callback<MultiStats>} callback signals when the call finishes
364 * @returns {void}
365 */
366 run(callback) {
367 if (this.running) {
368 return callback(new ConcurrentCompilationError());
369 }
370
371 const finalCallback = (err, stats) => {
372 this.running = false;
373
374 if (callback !== undefined) {
375 return callback(err, stats);
376 }
377 };
378
379 const allStats = this.compilers.map(() => null);
380 if (this.validateDependencies(callback)) {
381 this.running = true;
382 this.runWithDependencies(
383 this.compilers,
384 (compiler, callback) => {
385 const compilerIdx = this.compilers.indexOf(compiler);
386 compiler.run((err, stats) => {
387 if (err) {
388 return callback(err);
389 }
390 allStats[compilerIdx] = stats;
391 callback();
392 });
393 },
394 err => {
395 if (err) {
396 return finalCallback(err);
397 }
398 finalCallback(null, new MultiStats(allStats));
399 }
400 );
401 }
402 }
403
404 purgeInputFileSystem() {
405 for (const compiler of this.compilers) {
406 if (compiler.inputFileSystem && compiler.inputFileSystem.purge) {
407 compiler.inputFileSystem.purge();
408 }
409 }
410 }
411
412 /**
413 * @param {Callback<void>} callback signals when the compiler closes
414 * @returns {void}
415 */
416 close(callback) {
417 asyncLib.each(
418 this.compilers,
419 (compiler, callback) => {
420 compiler.close(callback);
421 },
422 callback
423 );
424 }
425};