UNPKG

36.2 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
549 const finalCallback = (err, entries, compilation) => {
550 try {
551 callback(err, entries, compilation);
552 } catch (e) {
553 const err = new WebpackError(
554 `compiler.runAsChild callback error: ${e}`
555 );
556 err.details = e.stack;
557 this.parentCompilation.errors.push(err);
558 }
559 };
560
561 this.compile((err, compilation) => {
562 if (err) return finalCallback(err);
563
564 this.parentCompilation.children.push(compilation);
565 for (const { name, source, info } of compilation.getAssets()) {
566 this.parentCompilation.emitAsset(name, source, info);
567 }
568
569 const entries = [];
570 for (const ep of compilation.entrypoints.values()) {
571 entries.push(...ep.chunks);
572 }
573
574 compilation.startTime = startTime;
575 compilation.endTime = Date.now();
576
577 return finalCallback(null, entries, compilation);
578 });
579 }
580
581 purgeInputFileSystem() {
582 if (this.inputFileSystem && this.inputFileSystem.purge) {
583 this.inputFileSystem.purge();
584 }
585 }
586
587 /**
588 * @param {Compilation} compilation the compilation
589 * @param {Callback<void>} callback signals when the assets are emitted
590 * @returns {void}
591 */
592 emitAssets(compilation, callback) {
593 let outputPath;
594
595 const emitFiles = err => {
596 if (err) return callback(err);
597
598 const assets = compilation.getAssets();
599 compilation.assets = { ...compilation.assets };
600 /** @type {Map<string, { path: string, source: Source, size: number, waiting: { cacheEntry: any, file: string }[] }>} */
601 const caseInsensitiveMap = new Map();
602 /** @type {Set<string>} */
603 const allTargetPaths = new Set();
604 asyncLib.forEachLimit(
605 assets,
606 15,
607 ({ name: file, source, info }, callback) => {
608 let targetFile = file;
609 let immutable = info.immutable;
610 const queryStringIdx = targetFile.indexOf("?");
611 if (queryStringIdx >= 0) {
612 targetFile = targetFile.slice(0, queryStringIdx);
613 // We may remove the hash, which is in the query string
614 // So we recheck if the file is immutable
615 // This doesn't cover all cases, but immutable is only a performance optimization anyway
616 immutable =
617 immutable &&
618 (includesHash(targetFile, info.contenthash) ||
619 includesHash(targetFile, info.chunkhash) ||
620 includesHash(targetFile, info.modulehash) ||
621 includesHash(targetFile, info.fullhash));
622 }
623
624 const writeOut = err => {
625 if (err) return callback(err);
626 const targetPath = join(
627 this.outputFileSystem,
628 outputPath,
629 targetFile
630 );
631 allTargetPaths.add(targetPath);
632
633 // check if the target file has already been written by this Compiler
634 const targetFileGeneration =
635 this._assetEmittingWrittenFiles.get(targetPath);
636
637 // create an cache entry for this Source if not already existing
638 let cacheEntry = this._assetEmittingSourceCache.get(source);
639 if (cacheEntry === undefined) {
640 cacheEntry = {
641 sizeOnlySource: undefined,
642 writtenTo: new Map()
643 };
644 this._assetEmittingSourceCache.set(source, cacheEntry);
645 }
646
647 let similarEntry;
648
649 const checkSimilarFile = () => {
650 const caseInsensitiveTargetPath = targetPath.toLowerCase();
651 similarEntry = caseInsensitiveMap.get(caseInsensitiveTargetPath);
652 if (similarEntry !== undefined) {
653 const { path: other, source: otherSource } = similarEntry;
654 if (isSourceEqual(otherSource, source)) {
655 // Size may or may not be available at this point.
656 // If it's not available add to "waiting" list and it will be updated once available
657 if (similarEntry.size !== undefined) {
658 updateWithReplacementSource(similarEntry.size);
659 } else {
660 if (!similarEntry.waiting) similarEntry.waiting = [];
661 similarEntry.waiting.push({ file, cacheEntry });
662 }
663 alreadyWritten();
664 } else {
665 const err =
666 new WebpackError(`Prevent writing to file that only differs in casing or query string from already written file.
667This will lead to a race-condition and corrupted files on case-insensitive file systems.
668${targetPath}
669${other}`);
670 err.file = file;
671 callback(err);
672 }
673 return true;
674 } else {
675 caseInsensitiveMap.set(
676 caseInsensitiveTargetPath,
677 (similarEntry = {
678 path: targetPath,
679 source,
680 size: undefined,
681 waiting: undefined
682 })
683 );
684 return false;
685 }
686 };
687
688 /**
689 * get the binary (Buffer) content from the Source
690 * @returns {Buffer} content for the source
691 */
692 const getContent = () => {
693 if (typeof source.buffer === "function") {
694 return source.buffer();
695 } else {
696 const bufferOrString = source.source();
697 if (Buffer.isBuffer(bufferOrString)) {
698 return bufferOrString;
699 } else {
700 return Buffer.from(bufferOrString, "utf8");
701 }
702 }
703 };
704
705 const alreadyWritten = () => {
706 // cache the information that the Source has been already been written to that location
707 if (targetFileGeneration === undefined) {
708 const newGeneration = 1;
709 this._assetEmittingWrittenFiles.set(targetPath, newGeneration);
710 cacheEntry.writtenTo.set(targetPath, newGeneration);
711 } else {
712 cacheEntry.writtenTo.set(targetPath, targetFileGeneration);
713 }
714 callback();
715 };
716
717 /**
718 * Write the file to output file system
719 * @param {Buffer} content content to be written
720 * @returns {void}
721 */
722 const doWrite = content => {
723 this.outputFileSystem.writeFile(targetPath, content, err => {
724 if (err) return callback(err);
725
726 // information marker that the asset has been emitted
727 compilation.emittedAssets.add(file);
728
729 // cache the information that the Source has been written to that location
730 const newGeneration =
731 targetFileGeneration === undefined
732 ? 1
733 : targetFileGeneration + 1;
734 cacheEntry.writtenTo.set(targetPath, newGeneration);
735 this._assetEmittingWrittenFiles.set(targetPath, newGeneration);
736 this.hooks.assetEmitted.callAsync(
737 file,
738 {
739 content,
740 source,
741 outputPath,
742 compilation,
743 targetPath
744 },
745 callback
746 );
747 });
748 };
749
750 const updateWithReplacementSource = size => {
751 updateFileWithReplacementSource(file, cacheEntry, size);
752 similarEntry.size = size;
753 if (similarEntry.waiting !== undefined) {
754 for (const { file, cacheEntry } of similarEntry.waiting) {
755 updateFileWithReplacementSource(file, cacheEntry, size);
756 }
757 }
758 };
759
760 const updateFileWithReplacementSource = (
761 file,
762 cacheEntry,
763 size
764 ) => {
765 // Create a replacement resource which only allows to ask for size
766 // This allows to GC all memory allocated by the Source
767 // (expect when the Source is stored in any other cache)
768 if (!cacheEntry.sizeOnlySource) {
769 cacheEntry.sizeOnlySource = new SizeOnlySource(size);
770 }
771 compilation.updateAsset(file, cacheEntry.sizeOnlySource, {
772 size
773 });
774 };
775
776 const processExistingFile = stats => {
777 // skip emitting if it's already there and an immutable file
778 if (immutable) {
779 updateWithReplacementSource(stats.size);
780 return alreadyWritten();
781 }
782
783 const content = getContent();
784
785 updateWithReplacementSource(content.length);
786
787 // if it exists and content on disk matches content
788 // skip writing the same content again
789 // (to keep mtime and don't trigger watchers)
790 // for a fast negative match file size is compared first
791 if (content.length === stats.size) {
792 compilation.comparedForEmitAssets.add(file);
793 return this.outputFileSystem.readFile(
794 targetPath,
795 (err, existingContent) => {
796 if (
797 err ||
798 !content.equals(/** @type {Buffer} */ (existingContent))
799 ) {
800 return doWrite(content);
801 } else {
802 return alreadyWritten();
803 }
804 }
805 );
806 }
807
808 return doWrite(content);
809 };
810
811 const processMissingFile = () => {
812 const content = getContent();
813
814 updateWithReplacementSource(content.length);
815
816 return doWrite(content);
817 };
818
819 // if the target file has already been written
820 if (targetFileGeneration !== undefined) {
821 // check if the Source has been written to this target file
822 const writtenGeneration = cacheEntry.writtenTo.get(targetPath);
823 if (writtenGeneration === targetFileGeneration) {
824 // if yes, we may skip writing the file
825 // if it's already there
826 // (we assume one doesn't modify files while the Compiler is running, other then removing them)
827
828 if (this._assetEmittingPreviousFiles.has(targetPath)) {
829 // We assume that assets from the last compilation say intact on disk (they are not removed)
830 compilation.updateAsset(file, cacheEntry.sizeOnlySource, {
831 size: cacheEntry.sizeOnlySource.size()
832 });
833
834 return callback();
835 } else {
836 // Settings immutable will make it accept file content without comparing when file exist
837 immutable = true;
838 }
839 } else if (!immutable) {
840 if (checkSimilarFile()) return;
841 // We wrote to this file before which has very likely a different content
842 // skip comparing and assume content is different for performance
843 // This case happens often during watch mode.
844 return processMissingFile();
845 }
846 }
847
848 if (checkSimilarFile()) return;
849 if (this.options.output.compareBeforeEmit) {
850 this.outputFileSystem.stat(targetPath, (err, stats) => {
851 const exists = !err && stats.isFile();
852
853 if (exists) {
854 processExistingFile(stats);
855 } else {
856 processMissingFile();
857 }
858 });
859 } else {
860 processMissingFile();
861 }
862 };
863
864 if (targetFile.match(/\/|\\/)) {
865 const fs = this.outputFileSystem;
866 const dir = dirname(fs, join(fs, outputPath, targetFile));
867 mkdirp(fs, dir, writeOut);
868 } else {
869 writeOut();
870 }
871 },
872 err => {
873 // Clear map to free up memory
874 caseInsensitiveMap.clear();
875 if (err) {
876 this._assetEmittingPreviousFiles.clear();
877 return callback(err);
878 }
879
880 this._assetEmittingPreviousFiles = allTargetPaths;
881
882 this.hooks.afterEmit.callAsync(compilation, err => {
883 if (err) return callback(err);
884
885 return callback();
886 });
887 }
888 );
889 };
890
891 this.hooks.emit.callAsync(compilation, err => {
892 if (err) return callback(err);
893 outputPath = compilation.getPath(this.outputPath, {});
894 mkdirp(this.outputFileSystem, outputPath, emitFiles);
895 });
896 }
897
898 /**
899 * @param {Callback<void>} callback signals when the call finishes
900 * @returns {void}
901 */
902 emitRecords(callback) {
903 if (this.hooks.emitRecords.isUsed()) {
904 if (this.recordsOutputPath) {
905 asyncLib.parallel(
906 [
907 cb => this.hooks.emitRecords.callAsync(cb),
908 this._emitRecords.bind(this)
909 ],
910 err => callback(err)
911 );
912 } else {
913 this.hooks.emitRecords.callAsync(callback);
914 }
915 } else {
916 if (this.recordsOutputPath) {
917 this._emitRecords(callback);
918 } else {
919 callback();
920 }
921 }
922 }
923
924 /**
925 * @param {Callback<void>} callback signals when the call finishes
926 * @returns {void}
927 */
928 _emitRecords(callback) {
929 const writeFile = () => {
930 this.outputFileSystem.writeFile(
931 this.recordsOutputPath,
932 JSON.stringify(
933 this.records,
934 (n, value) => {
935 if (
936 typeof value === "object" &&
937 value !== null &&
938 !Array.isArray(value)
939 ) {
940 const keys = Object.keys(value);
941 if (!isSorted(keys)) {
942 return sortObject(value, keys);
943 }
944 }
945 return value;
946 },
947 2
948 ),
949 callback
950 );
951 };
952
953 const recordsOutputPathDirectory = dirname(
954 this.outputFileSystem,
955 this.recordsOutputPath
956 );
957 if (!recordsOutputPathDirectory) {
958 return writeFile();
959 }
960 mkdirp(this.outputFileSystem, recordsOutputPathDirectory, err => {
961 if (err) return callback(err);
962 writeFile();
963 });
964 }
965
966 /**
967 * @param {Callback<void>} callback signals when the call finishes
968 * @returns {void}
969 */
970 readRecords(callback) {
971 if (this.hooks.readRecords.isUsed()) {
972 if (this.recordsInputPath) {
973 asyncLib.parallel([
974 cb => this.hooks.readRecords.callAsync(cb),
975 this._readRecords.bind(this)
976 ]);
977 } else {
978 this.records = {};
979 this.hooks.readRecords.callAsync(callback);
980 }
981 } else {
982 if (this.recordsInputPath) {
983 this._readRecords(callback);
984 } else {
985 this.records = {};
986 callback();
987 }
988 }
989 }
990
991 /**
992 * @param {Callback<void>} callback signals when the call finishes
993 * @returns {void}
994 */
995 _readRecords(callback) {
996 if (!this.recordsInputPath) {
997 this.records = {};
998 return callback();
999 }
1000 this.inputFileSystem.stat(this.recordsInputPath, err => {
1001 // It doesn't exist
1002 // We can ignore this.
1003 if (err) return callback();
1004
1005 this.inputFileSystem.readFile(this.recordsInputPath, (err, content) => {
1006 if (err) return callback(err);
1007
1008 try {
1009 this.records = parseJson(content.toString("utf-8"));
1010 } catch (e) {
1011 e.message = "Cannot parse records: " + e.message;
1012 return callback(e);
1013 }
1014
1015 return callback();
1016 });
1017 });
1018 }
1019
1020 /**
1021 * @param {Compilation} compilation the compilation
1022 * @param {string} compilerName the compiler's name
1023 * @param {number} compilerIndex the compiler's index
1024 * @param {OutputOptions=} outputOptions the output options
1025 * @param {WebpackPluginInstance[]=} plugins the plugins to apply
1026 * @returns {Compiler} a child compiler
1027 */
1028 createChildCompiler(
1029 compilation,
1030 compilerName,
1031 compilerIndex,
1032 outputOptions,
1033 plugins
1034 ) {
1035 const childCompiler = new Compiler(this.context, {
1036 ...this.options,
1037 output: {
1038 ...this.options.output,
1039 ...outputOptions
1040 }
1041 });
1042 childCompiler.name = compilerName;
1043 childCompiler.outputPath = this.outputPath;
1044 childCompiler.inputFileSystem = this.inputFileSystem;
1045 childCompiler.outputFileSystem = null;
1046 childCompiler.resolverFactory = this.resolverFactory;
1047 childCompiler.modifiedFiles = this.modifiedFiles;
1048 childCompiler.removedFiles = this.removedFiles;
1049 childCompiler.fileTimestamps = this.fileTimestamps;
1050 childCompiler.contextTimestamps = this.contextTimestamps;
1051 childCompiler.fsStartTime = this.fsStartTime;
1052 childCompiler.cache = this.cache;
1053 childCompiler.compilerPath = `${this.compilerPath}${compilerName}|${compilerIndex}|`;
1054 childCompiler._backCompat = this._backCompat;
1055
1056 const relativeCompilerName = makePathsRelative(
1057 this.context,
1058 compilerName,
1059 this.root
1060 );
1061 if (!this.records[relativeCompilerName]) {
1062 this.records[relativeCompilerName] = [];
1063 }
1064 if (this.records[relativeCompilerName][compilerIndex]) {
1065 childCompiler.records = this.records[relativeCompilerName][compilerIndex];
1066 } else {
1067 this.records[relativeCompilerName].push((childCompiler.records = {}));
1068 }
1069
1070 childCompiler.parentCompilation = compilation;
1071 childCompiler.root = this.root;
1072 if (Array.isArray(plugins)) {
1073 for (const plugin of plugins) {
1074 plugin.apply(childCompiler);
1075 }
1076 }
1077 for (const name in this.hooks) {
1078 if (
1079 ![
1080 "make",
1081 "compile",
1082 "emit",
1083 "afterEmit",
1084 "invalid",
1085 "done",
1086 "thisCompilation"
1087 ].includes(name)
1088 ) {
1089 if (childCompiler.hooks[name]) {
1090 childCompiler.hooks[name].taps = this.hooks[name].taps.slice();
1091 }
1092 }
1093 }
1094
1095 compilation.hooks.childCompiler.call(
1096 childCompiler,
1097 compilerName,
1098 compilerIndex
1099 );
1100
1101 return childCompiler;
1102 }
1103
1104 isChild() {
1105 return !!this.parentCompilation;
1106 }
1107
1108 createCompilation(params) {
1109 this._cleanupLastCompilation();
1110 return (this._lastCompilation = new Compilation(this, params));
1111 }
1112
1113 /**
1114 * @param {CompilationParams} params the compilation parameters
1115 * @returns {Compilation} the created compilation
1116 */
1117 newCompilation(params) {
1118 const compilation = this.createCompilation(params);
1119 compilation.name = this.name;
1120 compilation.records = this.records;
1121 this.hooks.thisCompilation.call(compilation, params);
1122 this.hooks.compilation.call(compilation, params);
1123 return compilation;
1124 }
1125
1126 createNormalModuleFactory() {
1127 this._cleanupLastNormalModuleFactory();
1128 const normalModuleFactory = new NormalModuleFactory({
1129 context: this.options.context,
1130 fs: this.inputFileSystem,
1131 resolverFactory: this.resolverFactory,
1132 options: this.options.module,
1133 associatedObjectForCache: this.root,
1134 layers: this.options.experiments.layers
1135 });
1136 this._lastNormalModuleFactory = normalModuleFactory;
1137 this.hooks.normalModuleFactory.call(normalModuleFactory);
1138 return normalModuleFactory;
1139 }
1140
1141 createContextModuleFactory() {
1142 const contextModuleFactory = new ContextModuleFactory(this.resolverFactory);
1143 this.hooks.contextModuleFactory.call(contextModuleFactory);
1144 return contextModuleFactory;
1145 }
1146
1147 newCompilationParams() {
1148 const params = {
1149 normalModuleFactory: this.createNormalModuleFactory(),
1150 contextModuleFactory: this.createContextModuleFactory()
1151 };
1152 return params;
1153 }
1154
1155 /**
1156 * @param {Callback<Compilation>} callback signals when the compilation finishes
1157 * @returns {void}
1158 */
1159 compile(callback) {
1160 const params = this.newCompilationParams();
1161 this.hooks.beforeCompile.callAsync(params, err => {
1162 if (err) return callback(err);
1163
1164 this.hooks.compile.call(params);
1165
1166 const compilation = this.newCompilation(params);
1167
1168 const logger = compilation.getLogger("webpack.Compiler");
1169
1170 logger.time("make hook");
1171 this.hooks.make.callAsync(compilation, err => {
1172 logger.timeEnd("make hook");
1173 if (err) return callback(err);
1174
1175 logger.time("finish make hook");
1176 this.hooks.finishMake.callAsync(compilation, err => {
1177 logger.timeEnd("finish make hook");
1178 if (err) return callback(err);
1179
1180 process.nextTick(() => {
1181 logger.time("finish compilation");
1182 compilation.finish(err => {
1183 logger.timeEnd("finish compilation");
1184 if (err) return callback(err);
1185
1186 logger.time("seal compilation");
1187 compilation.seal(err => {
1188 logger.timeEnd("seal compilation");
1189 if (err) return callback(err);
1190
1191 logger.time("afterCompile hook");
1192 this.hooks.afterCompile.callAsync(compilation, err => {
1193 logger.timeEnd("afterCompile hook");
1194 if (err) return callback(err);
1195
1196 return callback(null, compilation);
1197 });
1198 });
1199 });
1200 });
1201 });
1202 });
1203 });
1204 }
1205
1206 /**
1207 * @param {Callback<void>} callback signals when the compiler closes
1208 * @returns {void}
1209 */
1210 close(callback) {
1211 if (this.watching) {
1212 // When there is still an active watching, close this first
1213 this.watching.close(err => {
1214 this.close(callback);
1215 });
1216 return;
1217 }
1218 this.hooks.shutdown.callAsync(err => {
1219 if (err) return callback(err);
1220 // Get rid of reference to last compilation to avoid leaking memory
1221 // We can't run this._cleanupLastCompilation() as the Stats to this compilation
1222 // might be still in use. We try to get rid of the reference to the cache instead.
1223 this._lastCompilation = undefined;
1224 this._lastNormalModuleFactory = undefined;
1225 this.cache.shutdown(callback);
1226 });
1227 }
1228}
1229
1230module.exports = Compiler;