UNPKG

31.6 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 parseJson = require("json-parse-better-errors");
9const asyncLib = require("neo-async");
10const {
11 SyncHook,
12 SyncBailHook,
13 AsyncParallelHook,
14 AsyncSeriesHook
15} = require("tapable");
16const { SizeOnlySource } = require("webpack-sources");
17const webpack = require("./");
18const Cache = require("./Cache");
19const CacheFacade = require("./CacheFacade");
20const Compilation = require("./Compilation");
21const ConcurrentCompilationError = require("./ConcurrentCompilationError");
22const ContextModuleFactory = require("./ContextModuleFactory");
23const NormalModuleFactory = require("./NormalModuleFactory");
24const RequestShortener = require("./RequestShortener");
25const ResolverFactory = require("./ResolverFactory");
26const Stats = require("./Stats");
27const Watching = require("./Watching");
28const WebpackError = require("./WebpackError");
29const { Logger } = require("./logging/Logger");
30const { join, dirname, mkdirp } = require("./util/fs");
31const { makePathsRelative } = require("./util/identifier");
32const { isSourceEqual } = require("./util/source");
33
34/** @typedef {import("webpack-sources").Source} Source */
35/** @typedef {import("../declarations/WebpackOptions").EntryNormalized} Entry */
36/** @typedef {import("../declarations/WebpackOptions").OutputNormalized} OutputOptions */
37/** @typedef {import("../declarations/WebpackOptions").WatchOptions} WatchOptions */
38/** @typedef {import("../declarations/WebpackOptions").WebpackOptionsNormalized} WebpackOptions */
39/** @typedef {import("../declarations/WebpackOptions").WebpackPluginInstance} WebpackPluginInstance */
40/** @typedef {import("./Chunk")} Chunk */
41/** @typedef {import("./FileSystemInfo").FileSystemInfoEntry} FileSystemInfoEntry */
42/** @typedef {import("./Module")} Module */
43/** @typedef {import("./util/fs").InputFileSystem} InputFileSystem */
44/** @typedef {import("./util/fs").IntermediateFileSystem} IntermediateFileSystem */
45/** @typedef {import("./util/fs").OutputFileSystem} OutputFileSystem */
46/** @typedef {import("./util/fs").WatchFileSystem} WatchFileSystem */
47
48/**
49 * @typedef {Object} CompilationParams
50 * @property {NormalModuleFactory} normalModuleFactory
51 * @property {ContextModuleFactory} contextModuleFactory
52 */
53
54/**
55 * @template T
56 * @callback Callback
57 * @param {Error=} err
58 * @param {T=} result
59 */
60
61/**
62 * @callback RunAsChildCallback
63 * @param {Error=} err
64 * @param {Chunk[]=} entries
65 * @param {Compilation=} compilation
66 */
67
68/**
69 * @typedef {Object} AssetEmittedInfo
70 * @property {Buffer} content
71 * @property {Source} source
72 * @property {Compilation} compilation
73 * @property {string} outputPath
74 * @property {string} targetPath
75 */
76
77/**
78 * @param {string[]} array an array
79 * @returns {boolean} true, if the array is sorted
80 */
81const isSorted = array => {
82 for (let i = 1; i < array.length; i++) {
83 if (array[i - 1] > array[i]) return false;
84 }
85 return true;
86};
87
88/**
89 * @param {Object} obj an object
90 * @param {string[]} keys the keys of the object
91 * @returns {Object} the object with properties sorted by property name
92 */
93const sortObject = (obj, keys) => {
94 const o = {};
95 for (const k of keys.sort()) {
96 o[k] = obj[k];
97 }
98 return o;
99};
100
101/**
102 * @param {string} filename filename
103 * @param {string | string[] | undefined} hashes list of hashes
104 * @returns {boolean} true, if the filename contains any hash
105 */
106const includesHash = (filename, hashes) => {
107 if (!hashes) return false;
108 if (Array.isArray(hashes)) {
109 return hashes.some(hash => filename.includes(hash));
110 } else {
111 return filename.includes(hashes);
112 }
113};
114
115class Compiler {
116 /**
117 * @param {string} context the compilation path
118 */
119 constructor(context) {
120 this.hooks = Object.freeze({
121 /** @type {SyncHook<[]>} */
122 initialize: new SyncHook([]),
123
124 /** @type {SyncBailHook<[Compilation], boolean>} */
125 shouldEmit: new SyncBailHook(["compilation"]),
126 /** @type {AsyncSeriesHook<[Stats]>} */
127 done: new AsyncSeriesHook(["stats"]),
128 /** @type {SyncHook<[Stats]>} */
129 afterDone: new SyncHook(["stats"]),
130 /** @type {AsyncSeriesHook<[]>} */
131 additionalPass: new AsyncSeriesHook([]),
132 /** @type {AsyncSeriesHook<[Compiler]>} */
133 beforeRun: new AsyncSeriesHook(["compiler"]),
134 /** @type {AsyncSeriesHook<[Compiler]>} */
135 run: new AsyncSeriesHook(["compiler"]),
136 /** @type {AsyncSeriesHook<[Compilation]>} */
137 emit: new AsyncSeriesHook(["compilation"]),
138 /** @type {AsyncSeriesHook<[string, AssetEmittedInfo]>} */
139 assetEmitted: new AsyncSeriesHook(["file", "info"]),
140 /** @type {AsyncSeriesHook<[Compilation]>} */
141 afterEmit: new AsyncSeriesHook(["compilation"]),
142
143 /** @type {SyncHook<[Compilation, CompilationParams]>} */
144 thisCompilation: new SyncHook(["compilation", "params"]),
145 /** @type {SyncHook<[Compilation, CompilationParams]>} */
146 compilation: new SyncHook(["compilation", "params"]),
147 /** @type {SyncHook<[NormalModuleFactory]>} */
148 normalModuleFactory: new SyncHook(["normalModuleFactory"]),
149 /** @type {SyncHook<[ContextModuleFactory]>} */
150 contextModuleFactory: new SyncHook(["contextModuleFactory"]),
151
152 /** @type {AsyncSeriesHook<[CompilationParams]>} */
153 beforeCompile: new AsyncSeriesHook(["params"]),
154 /** @type {SyncHook<[CompilationParams]>} */
155 compile: new SyncHook(["params"]),
156 /** @type {AsyncParallelHook<[Compilation]>} */
157 make: new AsyncParallelHook(["compilation"]),
158 /** @type {AsyncParallelHook<[Compilation]>} */
159 finishMake: new AsyncSeriesHook(["compilation"]),
160 /** @type {AsyncSeriesHook<[Compilation]>} */
161 afterCompile: new AsyncSeriesHook(["compilation"]),
162
163 /** @type {AsyncSeriesHook<[Compiler]>} */
164 watchRun: new AsyncSeriesHook(["compiler"]),
165 /** @type {SyncHook<[Error]>} */
166 failed: new SyncHook(["error"]),
167 /** @type {SyncHook<[string | null, number]>} */
168 invalid: new SyncHook(["filename", "changeTime"]),
169 /** @type {SyncHook<[]>} */
170 watchClose: new SyncHook([]),
171 /** @type {AsyncSeriesHook<[]>} */
172 shutdown: new AsyncSeriesHook([]),
173
174 /** @type {SyncBailHook<[string, string, any[]], true>} */
175 infrastructureLog: new SyncBailHook(["origin", "type", "args"]),
176
177 // TODO the following hooks are weirdly located here
178 // TODO move them for webpack 5
179 /** @type {SyncHook<[]>} */
180 environment: new SyncHook([]),
181 /** @type {SyncHook<[]>} */
182 afterEnvironment: new SyncHook([]),
183 /** @type {SyncHook<[Compiler]>} */
184 afterPlugins: new SyncHook(["compiler"]),
185 /** @type {SyncHook<[Compiler]>} */
186 afterResolvers: new SyncHook(["compiler"]),
187 /** @type {SyncBailHook<[string, Entry], boolean>} */
188 entryOption: new SyncBailHook(["context", "entry"])
189 });
190
191 this.webpack = webpack;
192
193 /** @type {string=} */
194 this.name = undefined;
195 /** @type {Compilation=} */
196 this.parentCompilation = undefined;
197 /** @type {Compiler} */
198 this.root = this;
199 /** @type {string} */
200 this.outputPath = "";
201 /** @type {Watching} */
202 this.watching = undefined;
203
204 /** @type {OutputFileSystem} */
205 this.outputFileSystem = null;
206 /** @type {IntermediateFileSystem} */
207 this.intermediateFileSystem = null;
208 /** @type {InputFileSystem} */
209 this.inputFileSystem = null;
210 /** @type {WatchFileSystem} */
211 this.watchFileSystem = null;
212
213 /** @type {string|null} */
214 this.recordsInputPath = null;
215 /** @type {string|null} */
216 this.recordsOutputPath = null;
217 this.records = {};
218 /** @type {Set<string>} */
219 this.managedPaths = new Set();
220 /** @type {Set<string>} */
221 this.immutablePaths = new Set();
222
223 /** @type {Set<string>} */
224 this.modifiedFiles = undefined;
225 /** @type {Set<string>} */
226 this.removedFiles = undefined;
227 /** @type {Map<string, FileSystemInfoEntry | "ignore" | null>} */
228 this.fileTimestamps = undefined;
229 /** @type {Map<string, FileSystemInfoEntry | "ignore" | null>} */
230 this.contextTimestamps = undefined;
231
232 /** @type {ResolverFactory} */
233 this.resolverFactory = new ResolverFactory();
234
235 this.infrastructureLogger = undefined;
236
237 /** @type {WebpackOptions} */
238 this.options = /** @type {WebpackOptions} */ ({});
239
240 this.context = context;
241
242 this.requestShortener = new RequestShortener(context, this.root);
243
244 this.cache = new Cache();
245
246 this.compilerPath = "";
247
248 /** @type {boolean} */
249 this.running = false;
250
251 /** @type {boolean} */
252 this.idle = false;
253
254 /** @type {boolean} */
255 this.watchMode = false;
256
257 /** @private @type {WeakMap<Source, { sizeOnlySource: SizeOnlySource, writtenTo: Map<string, number> }>} */
258 this._assetEmittingSourceCache = new WeakMap();
259 /** @private @type {Map<string, number>} */
260 this._assetEmittingWrittenFiles = new Map();
261 }
262
263 /**
264 * @param {string} name cache name
265 * @returns {CacheFacade} the cache facade instance
266 */
267 getCache(name) {
268 return new CacheFacade(this.cache, `${this.compilerPath}${name}`);
269 }
270
271 /**
272 * @param {string | (function(): string)} name name of the logger, or function called once to get the logger name
273 * @returns {Logger} a logger with that name
274 */
275 getInfrastructureLogger(name) {
276 if (!name) {
277 throw new TypeError(
278 "Compiler.getInfrastructureLogger(name) called without a name"
279 );
280 }
281 return new Logger(
282 (type, args) => {
283 if (typeof name === "function") {
284 name = name();
285 if (!name) {
286 throw new TypeError(
287 "Compiler.getInfrastructureLogger(name) called with a function not returning a name"
288 );
289 }
290 }
291 if (this.hooks.infrastructureLog.call(name, type, args) === undefined) {
292 if (this.infrastructureLogger !== undefined) {
293 this.infrastructureLogger(name, type, args);
294 }
295 }
296 },
297 childName => {
298 if (typeof name === "function") {
299 if (typeof childName === "function") {
300 return this.getInfrastructureLogger(() => {
301 if (typeof name === "function") {
302 name = name();
303 if (!name) {
304 throw new TypeError(
305 "Compiler.getInfrastructureLogger(name) called with a function not returning a name"
306 );
307 }
308 }
309 if (typeof childName === "function") {
310 childName = childName();
311 if (!childName) {
312 throw new TypeError(
313 "Logger.getChildLogger(name) called with a function not returning a name"
314 );
315 }
316 }
317 return `${name}/${childName}`;
318 });
319 } else {
320 return this.getInfrastructureLogger(() => {
321 if (typeof name === "function") {
322 name = name();
323 if (!name) {
324 throw new TypeError(
325 "Compiler.getInfrastructureLogger(name) called with a function not returning a name"
326 );
327 }
328 }
329 return `${name}/${childName}`;
330 });
331 }
332 } else {
333 if (typeof childName === "function") {
334 return this.getInfrastructureLogger(() => {
335 if (typeof childName === "function") {
336 childName = childName();
337 if (!childName) {
338 throw new TypeError(
339 "Logger.getChildLogger(name) called with a function not returning a name"
340 );
341 }
342 }
343 return `${name}/${childName}`;
344 });
345 } else {
346 return this.getInfrastructureLogger(`${name}/${childName}`);
347 }
348 }
349 }
350 );
351 }
352
353 /**
354 * @param {WatchOptions} watchOptions the watcher's options
355 * @param {Callback<Stats>} handler signals when the call finishes
356 * @returns {Watching} a compiler watcher
357 */
358 watch(watchOptions, handler) {
359 if (this.running) {
360 return handler(new ConcurrentCompilationError());
361 }
362
363 this.running = true;
364 this.watchMode = true;
365 this.watching = new Watching(this, watchOptions, handler);
366 return this.watching;
367 }
368
369 /**
370 * @param {Callback<Stats>} callback signals when the call finishes
371 * @returns {void}
372 */
373 run(callback) {
374 if (this.running) {
375 return callback(new ConcurrentCompilationError());
376 }
377
378 let logger;
379
380 const finalCallback = (err, stats) => {
381 if (logger) logger.time("beginIdle");
382 this.idle = true;
383 this.cache.beginIdle();
384 this.idle = true;
385 if (logger) logger.timeEnd("beginIdle");
386 this.running = false;
387 if (err) {
388 this.hooks.failed.call(err);
389 }
390 if (callback !== undefined) callback(err, stats);
391 this.hooks.afterDone.call(stats);
392 };
393
394 const startTime = Date.now();
395
396 this.running = true;
397
398 const onCompiled = (err, compilation) => {
399 if (err) return finalCallback(err);
400
401 if (this.hooks.shouldEmit.call(compilation) === false) {
402 compilation.startTime = startTime;
403 compilation.endTime = Date.now();
404 const stats = new Stats(compilation);
405 this.hooks.done.callAsync(stats, err => {
406 if (err) return finalCallback(err);
407 return finalCallback(null, stats);
408 });
409 return;
410 }
411
412 process.nextTick(() => {
413 logger = compilation.getLogger("webpack.Compiler");
414 logger.time("emitAssets");
415 this.emitAssets(compilation, err => {
416 logger.timeEnd("emitAssets");
417 if (err) return finalCallback(err);
418
419 if (compilation.hooks.needAdditionalPass.call()) {
420 compilation.needAdditionalPass = true;
421
422 compilation.startTime = startTime;
423 compilation.endTime = Date.now();
424 logger.time("done hook");
425 const stats = new Stats(compilation);
426 this.hooks.done.callAsync(stats, err => {
427 logger.timeEnd("done hook");
428 if (err) return finalCallback(err);
429
430 this.hooks.additionalPass.callAsync(err => {
431 if (err) return finalCallback(err);
432 this.compile(onCompiled);
433 });
434 });
435 return;
436 }
437
438 logger.time("emitRecords");
439 this.emitRecords(err => {
440 logger.timeEnd("emitRecords");
441 if (err) return finalCallback(err);
442
443 compilation.startTime = startTime;
444 compilation.endTime = Date.now();
445 logger.time("done hook");
446 const stats = new Stats(compilation);
447 this.hooks.done.callAsync(stats, err => {
448 logger.timeEnd("done hook");
449 if (err) return finalCallback(err);
450 this.cache.storeBuildDependencies(
451 compilation.buildDependencies,
452 err => {
453 if (err) return finalCallback(err);
454 return finalCallback(null, stats);
455 }
456 );
457 });
458 });
459 });
460 });
461 };
462
463 const run = () => {
464 this.hooks.beforeRun.callAsync(this, err => {
465 if (err) return finalCallback(err);
466
467 this.hooks.run.callAsync(this, err => {
468 if (err) return finalCallback(err);
469
470 this.readRecords(err => {
471 if (err) return finalCallback(err);
472
473 this.compile(onCompiled);
474 });
475 });
476 });
477 };
478
479 if (this.idle) {
480 this.cache.endIdle(err => {
481 if (err) return finalCallback(err);
482
483 this.idle = false;
484 run();
485 });
486 } else {
487 run();
488 }
489 }
490
491 /**
492 * @param {RunAsChildCallback} callback signals when the call finishes
493 * @returns {void}
494 */
495 runAsChild(callback) {
496 const startTime = Date.now();
497 this.compile((err, compilation) => {
498 if (err) return callback(err);
499
500 this.parentCompilation.children.push(compilation);
501 for (const { name, source, info } of compilation.getAssets()) {
502 this.parentCompilation.emitAsset(name, source, info);
503 }
504
505 const entries = [];
506 for (const ep of compilation.entrypoints.values()) {
507 entries.push(...ep.chunks);
508 }
509
510 compilation.startTime = startTime;
511 compilation.endTime = Date.now();
512
513 return callback(null, entries, compilation);
514 });
515 }
516
517 purgeInputFileSystem() {
518 if (this.inputFileSystem && this.inputFileSystem.purge) {
519 this.inputFileSystem.purge();
520 }
521 }
522
523 /**
524 * @param {Compilation} compilation the compilation
525 * @param {Callback<void>} callback signals when the assets are emitted
526 * @returns {void}
527 */
528 emitAssets(compilation, callback) {
529 let outputPath;
530
531 const emitFiles = err => {
532 if (err) return callback(err);
533
534 const assets = compilation.getAssets();
535 compilation.assets = { ...compilation.assets };
536 /** @type {Map<string, { path: string, source: Source, size: number, waiting: { cacheEntry: any, file: string }[] }>} */
537 const caseInsensitiveMap = new Map();
538 asyncLib.forEachLimit(
539 assets,
540 15,
541 ({ name: file, source, info }, callback) => {
542 let targetFile = file;
543 let immutable = info.immutable;
544 const queryStringIdx = targetFile.indexOf("?");
545 if (queryStringIdx >= 0) {
546 targetFile = targetFile.substr(0, queryStringIdx);
547 // We may remove the hash, which is in the query string
548 // So we recheck if the file is immutable
549 // This doesn't cover all cases, but immutable is only a performance optimization anyway
550 immutable =
551 immutable &&
552 (includesHash(targetFile, info.contenthash) ||
553 includesHash(targetFile, info.chunkhash) ||
554 includesHash(targetFile, info.modulehash) ||
555 includesHash(targetFile, info.fullhash));
556 }
557
558 const writeOut = err => {
559 if (err) return callback(err);
560 const targetPath = join(
561 this.outputFileSystem,
562 outputPath,
563 targetFile
564 );
565
566 // check if the target file has already been written by this Compiler
567 const targetFileGeneration = this._assetEmittingWrittenFiles.get(
568 targetPath
569 );
570
571 // create an cache entry for this Source if not already existing
572 let cacheEntry = this._assetEmittingSourceCache.get(source);
573 if (cacheEntry === undefined) {
574 cacheEntry = {
575 sizeOnlySource: undefined,
576 writtenTo: new Map()
577 };
578 this._assetEmittingSourceCache.set(source, cacheEntry);
579 }
580
581 let similarEntry;
582
583 const checkSimilarFile = () => {
584 const caseInsensitiveTargetPath = targetPath.toLowerCase();
585 similarEntry = caseInsensitiveMap.get(caseInsensitiveTargetPath);
586 if (similarEntry !== undefined) {
587 const { path: other, source: otherSource } = similarEntry;
588 if (isSourceEqual(otherSource, source)) {
589 // Size may or may not be available at this point.
590 // If it's not available add to "waiting" list and it will be updated once available
591 if (similarEntry.size !== undefined) {
592 updateWithReplacementSource(similarEntry.size);
593 } else {
594 if (!similarEntry.waiting) similarEntry.waiting = [];
595 similarEntry.waiting.push({ file, cacheEntry });
596 }
597 alreadyWritten();
598 } else {
599 const err = new WebpackError(`Prevent writing to file that only differs in casing or query string from already written file.
600This will lead to a race-condition and corrupted files on case-insensitive file systems.
601${targetPath}
602${other}`);
603 err.file = file;
604 callback(err);
605 }
606 return true;
607 } else {
608 caseInsensitiveMap.set(
609 caseInsensitiveTargetPath,
610 (similarEntry = {
611 path: targetPath,
612 source,
613 size: undefined,
614 waiting: undefined
615 })
616 );
617 return false;
618 }
619 };
620
621 /**
622 * get the binary (Buffer) content from the Source
623 * @returns {Buffer} content for the source
624 */
625 const getContent = () => {
626 if (typeof source.buffer === "function") {
627 return source.buffer();
628 } else {
629 const bufferOrString = source.source();
630 if (Buffer.isBuffer(bufferOrString)) {
631 return bufferOrString;
632 } else {
633 return Buffer.from(bufferOrString, "utf8");
634 }
635 }
636 };
637
638 const alreadyWritten = () => {
639 // cache the information that the Source has been already been written to that location
640 if (targetFileGeneration === undefined) {
641 const newGeneration = 1;
642 this._assetEmittingWrittenFiles.set(targetPath, newGeneration);
643 cacheEntry.writtenTo.set(targetPath, newGeneration);
644 } else {
645 cacheEntry.writtenTo.set(targetPath, targetFileGeneration);
646 }
647 callback();
648 };
649
650 /**
651 * Write the file to output file system
652 * @param {Buffer} content content to be written
653 * @returns {void}
654 */
655 const doWrite = content => {
656 this.outputFileSystem.writeFile(targetPath, content, err => {
657 if (err) return callback(err);
658
659 // information marker that the asset has been emitted
660 compilation.emittedAssets.add(file);
661
662 // cache the information that the Source has been written to that location
663 const newGeneration =
664 targetFileGeneration === undefined
665 ? 1
666 : targetFileGeneration + 1;
667 cacheEntry.writtenTo.set(targetPath, newGeneration);
668 this._assetEmittingWrittenFiles.set(targetPath, newGeneration);
669 this.hooks.assetEmitted.callAsync(
670 file,
671 {
672 content,
673 source,
674 outputPath,
675 compilation,
676 targetPath
677 },
678 callback
679 );
680 });
681 };
682
683 const updateWithReplacementSource = size => {
684 updateFileWithReplacementSource(file, cacheEntry, size);
685 similarEntry.size = size;
686 if (similarEntry.waiting !== undefined) {
687 for (const { file, cacheEntry } of similarEntry.waiting) {
688 updateFileWithReplacementSource(file, cacheEntry, size);
689 }
690 }
691 };
692
693 const updateFileWithReplacementSource = (
694 file,
695 cacheEntry,
696 size
697 ) => {
698 // Create a replacement resource which only allows to ask for size
699 // This allows to GC all memory allocated by the Source
700 // (expect when the Source is stored in any other cache)
701 if (!cacheEntry.sizeOnlySource) {
702 cacheEntry.sizeOnlySource = new SizeOnlySource(size);
703 }
704 compilation.updateAsset(file, cacheEntry.sizeOnlySource, {
705 size
706 });
707 };
708
709 const processExistingFile = stats => {
710 // skip emitting if it's already there and an immutable file
711 if (immutable) {
712 updateWithReplacementSource(stats.size);
713 return alreadyWritten();
714 }
715
716 const content = getContent();
717
718 updateWithReplacementSource(content.length);
719
720 // if it exists and content on disk matches content
721 // skip writing the same content again
722 // (to keep mtime and don't trigger watchers)
723 // for a fast negative match file size is compared first
724 if (content.length === stats.size) {
725 compilation.comparedForEmitAssets.add(file);
726 return this.outputFileSystem.readFile(
727 targetPath,
728 (err, existingContent) => {
729 if (
730 err ||
731 !content.equals(/** @type {Buffer} */ (existingContent))
732 ) {
733 return doWrite(content);
734 } else {
735 return alreadyWritten();
736 }
737 }
738 );
739 }
740
741 return doWrite(content);
742 };
743
744 const processMissingFile = () => {
745 const content = getContent();
746
747 updateWithReplacementSource(content.length);
748
749 return doWrite(content);
750 };
751
752 // if the target file has already been written
753 if (targetFileGeneration !== undefined) {
754 // check if the Source has been written to this target file
755 const writtenGeneration = cacheEntry.writtenTo.get(targetPath);
756 if (writtenGeneration === targetFileGeneration) {
757 // if yes, we skip writing the file
758 // as it's already there
759 // (we assume one doesn't remove files while the Compiler is running)
760
761 compilation.updateAsset(file, cacheEntry.sizeOnlySource, {
762 size: cacheEntry.sizeOnlySource.size()
763 });
764
765 return callback();
766 }
767
768 if (!immutable) {
769 if (checkSimilarFile()) return;
770 // We wrote to this file before which has very likely a different content
771 // skip comparing and assume content is different for performance
772 // This case happens often during watch mode.
773 return processMissingFile();
774 }
775 }
776
777 if (checkSimilarFile()) return;
778 if (this.options.output.compareBeforeEmit) {
779 this.outputFileSystem.stat(targetPath, (err, stats) => {
780 const exists = !err && stats.isFile();
781
782 if (exists) {
783 processExistingFile(stats);
784 } else {
785 processMissingFile();
786 }
787 });
788 } else {
789 processMissingFile();
790 }
791 };
792
793 if (targetFile.match(/\/|\\/)) {
794 const fs = this.outputFileSystem;
795 const dir = dirname(fs, join(fs, outputPath, targetFile));
796 mkdirp(fs, dir, writeOut);
797 } else {
798 writeOut();
799 }
800 },
801 err => {
802 if (err) return callback(err);
803
804 this.hooks.afterEmit.callAsync(compilation, err => {
805 if (err) return callback(err);
806
807 return callback();
808 });
809 }
810 );
811 };
812
813 this.hooks.emit.callAsync(compilation, err => {
814 if (err) return callback(err);
815 outputPath = compilation.getPath(this.outputPath, {});
816 mkdirp(this.outputFileSystem, outputPath, emitFiles);
817 });
818 }
819
820 /**
821 * @param {Callback<void>} callback signals when the call finishes
822 * @returns {void}
823 */
824 emitRecords(callback) {
825 if (!this.recordsOutputPath) return callback();
826
827 const writeFile = () => {
828 this.outputFileSystem.writeFile(
829 this.recordsOutputPath,
830 JSON.stringify(
831 this.records,
832 (n, value) => {
833 if (
834 typeof value === "object" &&
835 value !== null &&
836 !Array.isArray(value)
837 ) {
838 const keys = Object.keys(value);
839 if (!isSorted(keys)) {
840 return sortObject(value, keys);
841 }
842 }
843 return value;
844 },
845 2
846 ),
847 callback
848 );
849 };
850
851 const recordsOutputPathDirectory = dirname(
852 this.outputFileSystem,
853 this.recordsOutputPath
854 );
855 if (!recordsOutputPathDirectory) {
856 return writeFile();
857 }
858 mkdirp(this.outputFileSystem, recordsOutputPathDirectory, err => {
859 if (err) return callback(err);
860 writeFile();
861 });
862 }
863
864 /**
865 * @param {Callback<void>} callback signals when the call finishes
866 * @returns {void}
867 */
868 readRecords(callback) {
869 if (!this.recordsInputPath) {
870 this.records = {};
871 return callback();
872 }
873 this.inputFileSystem.stat(this.recordsInputPath, err => {
874 // It doesn't exist
875 // We can ignore this.
876 if (err) return callback();
877
878 this.inputFileSystem.readFile(this.recordsInputPath, (err, content) => {
879 if (err) return callback(err);
880
881 try {
882 this.records = parseJson(content.toString("utf-8"));
883 } catch (e) {
884 e.message = "Cannot parse records: " + e.message;
885 return callback(e);
886 }
887
888 return callback();
889 });
890 });
891 }
892
893 /**
894 * @param {Compilation} compilation the compilation
895 * @param {string} compilerName the compiler's name
896 * @param {number} compilerIndex the compiler's index
897 * @param {OutputOptions} outputOptions the output options
898 * @param {WebpackPluginInstance[]} plugins the plugins to apply
899 * @returns {Compiler} a child compiler
900 */
901 createChildCompiler(
902 compilation,
903 compilerName,
904 compilerIndex,
905 outputOptions,
906 plugins
907 ) {
908 const childCompiler = new Compiler(this.context);
909 childCompiler.name = compilerName;
910 childCompiler.outputPath = this.outputPath;
911 childCompiler.inputFileSystem = this.inputFileSystem;
912 childCompiler.outputFileSystem = null;
913 childCompiler.resolverFactory = this.resolverFactory;
914 childCompiler.modifiedFiles = this.modifiedFiles;
915 childCompiler.removedFiles = this.removedFiles;
916 childCompiler.fileTimestamps = this.fileTimestamps;
917 childCompiler.contextTimestamps = this.contextTimestamps;
918 childCompiler.cache = this.cache;
919 childCompiler.compilerPath = `${this.compilerPath}${compilerName}|${compilerIndex}|`;
920
921 const relativeCompilerName = makePathsRelative(
922 this.context,
923 compilerName,
924 this.root
925 );
926 if (!this.records[relativeCompilerName]) {
927 this.records[relativeCompilerName] = [];
928 }
929 if (this.records[relativeCompilerName][compilerIndex]) {
930 childCompiler.records = this.records[relativeCompilerName][compilerIndex];
931 } else {
932 this.records[relativeCompilerName].push((childCompiler.records = {}));
933 }
934
935 childCompiler.options = {
936 ...this.options,
937 output: {
938 ...this.options.output,
939 ...outputOptions
940 }
941 };
942 childCompiler.parentCompilation = compilation;
943 childCompiler.root = this.root;
944 if (Array.isArray(plugins)) {
945 for (const plugin of plugins) {
946 plugin.apply(childCompiler);
947 }
948 }
949 for (const name in this.hooks) {
950 if (
951 ![
952 "make",
953 "compile",
954 "emit",
955 "afterEmit",
956 "invalid",
957 "done",
958 "thisCompilation"
959 ].includes(name)
960 ) {
961 if (childCompiler.hooks[name]) {
962 childCompiler.hooks[name].taps = this.hooks[name].taps.slice();
963 }
964 }
965 }
966
967 compilation.hooks.childCompiler.call(
968 childCompiler,
969 compilerName,
970 compilerIndex
971 );
972
973 return childCompiler;
974 }
975
976 isChild() {
977 return !!this.parentCompilation;
978 }
979
980 createCompilation() {
981 return new Compilation(this);
982 }
983
984 /**
985 * @param {CompilationParams} params the compilation parameters
986 * @returns {Compilation} the created compilation
987 */
988 newCompilation(params) {
989 const compilation = this.createCompilation();
990 compilation.name = this.name;
991 compilation.records = this.records;
992 this.hooks.thisCompilation.call(compilation, params);
993 this.hooks.compilation.call(compilation, params);
994 return compilation;
995 }
996
997 createNormalModuleFactory() {
998 const normalModuleFactory = new NormalModuleFactory({
999 context: this.options.context,
1000 fs: this.inputFileSystem,
1001 resolverFactory: this.resolverFactory,
1002 options: this.options.module,
1003 associatedObjectForCache: this.root,
1004 layers: this.options.experiments.layers
1005 });
1006 this.hooks.normalModuleFactory.call(normalModuleFactory);
1007 return normalModuleFactory;
1008 }
1009
1010 createContextModuleFactory() {
1011 const contextModuleFactory = new ContextModuleFactory(this.resolverFactory);
1012 this.hooks.contextModuleFactory.call(contextModuleFactory);
1013 return contextModuleFactory;
1014 }
1015
1016 newCompilationParams() {
1017 const params = {
1018 normalModuleFactory: this.createNormalModuleFactory(),
1019 contextModuleFactory: this.createContextModuleFactory()
1020 };
1021 return params;
1022 }
1023
1024 /**
1025 * @param {Callback<Compilation>} callback signals when the compilation finishes
1026 * @returns {void}
1027 */
1028 compile(callback) {
1029 const params = this.newCompilationParams();
1030 this.hooks.beforeCompile.callAsync(params, err => {
1031 if (err) return callback(err);
1032
1033 this.hooks.compile.call(params);
1034
1035 const compilation = this.newCompilation(params);
1036
1037 const logger = compilation.getLogger("webpack.Compiler");
1038
1039 logger.time("make hook");
1040 this.hooks.make.callAsync(compilation, err => {
1041 logger.timeEnd("make hook");
1042 if (err) return callback(err);
1043
1044 logger.time("finish make hook");
1045 this.hooks.finishMake.callAsync(compilation, err => {
1046 logger.timeEnd("finish make hook");
1047 if (err) return callback(err);
1048
1049 process.nextTick(() => {
1050 logger.time("finish compilation");
1051 compilation.finish(err => {
1052 logger.timeEnd("finish compilation");
1053 if (err) return callback(err);
1054
1055 logger.time("seal compilation");
1056 compilation.seal(err => {
1057 logger.timeEnd("seal compilation");
1058 if (err) return callback(err);
1059
1060 logger.time("afterCompile hook");
1061 this.hooks.afterCompile.callAsync(compilation, err => {
1062 logger.timeEnd("afterCompile hook");
1063 if (err) return callback(err);
1064
1065 return callback(null, compilation);
1066 });
1067 });
1068 });
1069 });
1070 });
1071 });
1072 });
1073 }
1074
1075 /**
1076 * @param {Callback<void>} callback signals when the compiler closes
1077 * @returns {void}
1078 */
1079 close(callback) {
1080 this.hooks.shutdown.callAsync(err => {
1081 if (err) return callback(err);
1082 this.cache.shutdown(callback);
1083 });
1084 }
1085}
1086
1087module.exports = Compiler;