UNPKG

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