UNPKG

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