UNPKG

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