UNPKG

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