UNPKG

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