UNPKG

83.3 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 { create: createResolver } = require("enhanced-resolve");
9const asyncLib = require("neo-async");
10const AsyncQueue = require("./util/AsyncQueue");
11const createHash = require("./util/createHash");
12const { join, dirname, relative } = require("./util/fs");
13const makeSerializable = require("./util/makeSerializable");
14const processAsyncTree = require("./util/processAsyncTree");
15
16/** @typedef {import("./WebpackError")} WebpackError */
17/** @typedef {import("./logging/Logger").Logger} Logger */
18/** @typedef {import("./util/fs").InputFileSystem} InputFileSystem */
19
20const supportsEsm = +process.versions.modules >= 83;
21
22let FS_ACCURACY = 2000;
23
24const EMPTY_SET = new Set();
25
26const RBDT_RESOLVE_CJS = 0;
27const RBDT_RESOLVE_ESM = 1;
28const RBDT_RESOLVE_DIRECTORY = 2;
29const RBDT_RESOLVE_CJS_FILE = 3;
30const RBDT_RESOLVE_ESM_FILE = 4;
31const RBDT_DIRECTORY = 5;
32const RBDT_FILE = 6;
33const RBDT_DIRECTORY_DEPENDENCIES = 7;
34const RBDT_FILE_DEPENDENCIES = 8;
35
36const INVALID = Symbol("invalid");
37
38/**
39 * @typedef {Object} FileSystemInfoEntry
40 * @property {number} safeTime
41 * @property {number=} timestamp
42 * @property {string=} timestampHash
43 */
44
45/**
46 * @typedef {Object} TimestampAndHash
47 * @property {number} safeTime
48 * @property {number=} timestamp
49 * @property {string=} timestampHash
50 * @property {string} hash
51 */
52
53/**
54 * @typedef {Object} SnapshotOptimizationEntry
55 * @property {Snapshot} snapshot
56 * @property {number} shared
57 * @property {Set<string>} snapshotContent
58 * @property {Set<SnapshotOptimizationEntry>} children
59 */
60
61/**
62 * @typedef {Object} ResolveBuildDependenciesResult
63 * @property {Set<string>} files list of files
64 * @property {Set<string>} directories list of directories
65 * @property {Set<string>} missing list of missing entries
66 * @property {Map<string, string>} resolveResults stored resolve results
67 * @property {Object} resolveDependencies dependencies of the resolving
68 * @property {Set<string>} resolveDependencies.files list of files
69 * @property {Set<string>} resolveDependencies.directories list of directories
70 * @property {Set<string>} resolveDependencies.missing list of missing entries
71 */
72
73const DONE_ITERATOR_RESULT = new Set().keys().next();
74
75// cspell:word tshs
76// Tsh = Timestamp + Hash
77// Tshs = Timestamp + Hash combinations
78
79class Snapshot {
80 constructor() {
81 this._flags = 0;
82 /** @type {number | undefined} */
83 this.startTime = undefined;
84 /** @type {Map<string, FileSystemInfoEntry> | undefined} */
85 this.fileTimestamps = undefined;
86 /** @type {Map<string, string> | undefined} */
87 this.fileHashes = undefined;
88 /** @type {Map<string, TimestampAndHash | string> | undefined} */
89 this.fileTshs = undefined;
90 /** @type {Map<string, FileSystemInfoEntry> | undefined} */
91 this.contextTimestamps = undefined;
92 /** @type {Map<string, string> | undefined} */
93 this.contextHashes = undefined;
94 /** @type {Map<string, TimestampAndHash | string> | undefined} */
95 this.contextTshs = undefined;
96 /** @type {Map<string, boolean> | undefined} */
97 this.missingExistence = undefined;
98 /** @type {Map<string, string> | undefined} */
99 this.managedItemInfo = undefined;
100 /** @type {Set<string> | undefined} */
101 this.managedFiles = undefined;
102 /** @type {Set<string> | undefined} */
103 this.managedContexts = undefined;
104 /** @type {Set<string> | undefined} */
105 this.managedMissing = undefined;
106 /** @type {Set<Snapshot> | undefined} */
107 this.children = undefined;
108 }
109
110 hasStartTime() {
111 return (this._flags & 1) !== 0;
112 }
113
114 setStartTime(value) {
115 this._flags = this._flags | 1;
116 this.startTime = value;
117 }
118
119 setMergedStartTime(value, snapshot) {
120 if (value) {
121 if (snapshot.hasStartTime()) {
122 this.setStartTime(Math.min(value, snapshot.startTime));
123 } else {
124 this.setStartTime(value);
125 }
126 } else {
127 if (snapshot.hasStartTime()) this.setStartTime(snapshot.startTime);
128 }
129 }
130
131 hasFileTimestamps() {
132 return (this._flags & 2) !== 0;
133 }
134
135 setFileTimestamps(value) {
136 this._flags = this._flags | 2;
137 this.fileTimestamps = value;
138 }
139
140 hasFileHashes() {
141 return (this._flags & 4) !== 0;
142 }
143
144 setFileHashes(value) {
145 this._flags = this._flags | 4;
146 this.fileHashes = value;
147 }
148
149 hasFileTshs() {
150 return (this._flags & 8) !== 0;
151 }
152
153 setFileTshs(value) {
154 this._flags = this._flags | 8;
155 this.fileTshs = value;
156 }
157
158 hasContextTimestamps() {
159 return (this._flags & 0x10) !== 0;
160 }
161
162 setContextTimestamps(value) {
163 this._flags = this._flags | 0x10;
164 this.contextTimestamps = value;
165 }
166
167 hasContextHashes() {
168 return (this._flags & 0x20) !== 0;
169 }
170
171 setContextHashes(value) {
172 this._flags = this._flags | 0x20;
173 this.contextHashes = value;
174 }
175
176 hasContextTshs() {
177 return (this._flags & 0x40) !== 0;
178 }
179
180 setContextTshs(value) {
181 this._flags = this._flags | 0x40;
182 this.contextTshs = value;
183 }
184
185 hasMissingExistence() {
186 return (this._flags & 0x80) !== 0;
187 }
188
189 setMissingExistence(value) {
190 this._flags = this._flags | 0x80;
191 this.missingExistence = value;
192 }
193
194 hasManagedItemInfo() {
195 return (this._flags & 0x100) !== 0;
196 }
197
198 setManagedItemInfo(value) {
199 this._flags = this._flags | 0x100;
200 this.managedItemInfo = value;
201 }
202
203 hasManagedFiles() {
204 return (this._flags & 0x200) !== 0;
205 }
206
207 setManagedFiles(value) {
208 this._flags = this._flags | 0x200;
209 this.managedFiles = value;
210 }
211
212 hasManagedContexts() {
213 return (this._flags & 0x400) !== 0;
214 }
215
216 setManagedContexts(value) {
217 this._flags = this._flags | 0x400;
218 this.managedContexts = value;
219 }
220
221 hasManagedMissing() {
222 return (this._flags & 0x800) !== 0;
223 }
224
225 setManagedMissing(value) {
226 this._flags = this._flags | 0x800;
227 this.managedMissing = value;
228 }
229
230 hasChildren() {
231 return (this._flags & 0x1000) !== 0;
232 }
233
234 setChildren(value) {
235 this._flags = this._flags | 0x1000;
236 this.children = value;
237 }
238
239 addChild(child) {
240 if (!this.hasChildren()) {
241 this.setChildren(new Set());
242 }
243 this.children.add(child);
244 }
245
246 serialize({ write }) {
247 write(this._flags);
248 if (this.hasStartTime()) write(this.startTime);
249 if (this.hasFileTimestamps()) write(this.fileTimestamps);
250 if (this.hasFileHashes()) write(this.fileHashes);
251 if (this.hasFileTshs()) write(this.fileTshs);
252 if (this.hasContextTimestamps()) write(this.contextTimestamps);
253 if (this.hasContextHashes()) write(this.contextHashes);
254 if (this.hasContextTshs()) write(this.contextTshs);
255 if (this.hasMissingExistence()) write(this.missingExistence);
256 if (this.hasManagedItemInfo()) write(this.managedItemInfo);
257 if (this.hasManagedFiles()) write(this.managedFiles);
258 if (this.hasManagedContexts()) write(this.managedContexts);
259 if (this.hasManagedMissing()) write(this.managedMissing);
260 if (this.hasChildren()) write(this.children);
261 }
262
263 deserialize({ read }) {
264 this._flags = read();
265 if (this.hasStartTime()) this.startTime = read();
266 if (this.hasFileTimestamps()) this.fileTimestamps = read();
267 if (this.hasFileHashes()) this.fileHashes = read();
268 if (this.hasFileTshs()) this.fileTshs = read();
269 if (this.hasContextTimestamps()) this.contextTimestamps = read();
270 if (this.hasContextHashes()) this.contextHashes = read();
271 if (this.hasContextTshs()) this.contextTshs = read();
272 if (this.hasMissingExistence()) this.missingExistence = read();
273 if (this.hasManagedItemInfo()) this.managedItemInfo = read();
274 if (this.hasManagedFiles()) this.managedFiles = read();
275 if (this.hasManagedContexts()) this.managedContexts = read();
276 if (this.hasManagedMissing()) this.managedMissing = read();
277 if (this.hasChildren()) this.children = read();
278 }
279
280 /**
281 * @param {function(Snapshot): (Map<string, any> | Set<string>)[]} getMaps first
282 * @returns {Iterable<string>} iterable
283 */
284 _createIterable(getMaps) {
285 let snapshot = this;
286 return {
287 [Symbol.iterator]() {
288 let state = 0;
289 /** @type {IterableIterator<string>} */
290 let it;
291 let maps = getMaps(snapshot);
292 const queue = [];
293 return {
294 next() {
295 for (;;) {
296 switch (state) {
297 case 0:
298 if (maps.length > 0) {
299 const map = maps.pop();
300 if (map !== undefined) {
301 it = map.keys();
302 state = 1;
303 } else {
304 break;
305 }
306 } else {
307 state = 2;
308 break;
309 }
310 /* falls through */
311 case 1: {
312 const result = it.next();
313 if (!result.done) return result;
314 state = 0;
315 break;
316 }
317 case 2: {
318 const children = snapshot.children;
319 if (children !== undefined) {
320 for (const child of children) {
321 queue.push(child);
322 }
323 }
324 if (queue.length > 0) {
325 snapshot = queue.pop();
326 maps = getMaps(snapshot);
327 state = 0;
328 break;
329 } else {
330 state = 3;
331 }
332 }
333 /* falls through */
334 case 3:
335 return DONE_ITERATOR_RESULT;
336 }
337 }
338 }
339 };
340 }
341 };
342 }
343
344 /**
345 * @returns {Iterable<string>} iterable
346 */
347 getFileIterable() {
348 return this._createIterable(s => [
349 s.fileTimestamps,
350 s.fileHashes,
351 s.fileTshs,
352 s.managedFiles
353 ]);
354 }
355
356 /**
357 * @returns {Iterable<string>} iterable
358 */
359 getContextIterable() {
360 return this._createIterable(s => [
361 s.contextTimestamps,
362 s.contextHashes,
363 s.contextTshs,
364 s.managedContexts
365 ]);
366 }
367
368 /**
369 * @returns {Iterable<string>} iterable
370 */
371 getMissingIterable() {
372 return this._createIterable(s => [s.missingExistence, s.managedMissing]);
373 }
374}
375
376makeSerializable(Snapshot, "webpack/lib/FileSystemInfo", "Snapshot");
377
378const MIN_COMMON_SNAPSHOT_SIZE = 3;
379
380/**
381 * @template T
382 */
383class SnapshotOptimization {
384 /**
385 * @param {function(Snapshot): boolean} has has value
386 * @param {function(Snapshot): Map<string, T> | Set<string>} get get value
387 * @param {function(Snapshot, Map<string, T> | Set<string>): void} set set value
388 * @param {boolean=} isSet value is an Set instead of a Map
389 */
390 constructor(has, get, set, isSet = false) {
391 this._has = has;
392 this._get = get;
393 this._set = set;
394 this._isSet = isSet;
395 /** @type {Map<string, SnapshotOptimizationEntry>} */
396 this._map = new Map();
397 this._statItemsShared = 0;
398 this._statItemsUnshared = 0;
399 this._statSharedSnapshots = 0;
400 this._statReusedSharedSnapshots = 0;
401 }
402
403 getStatisticMessage() {
404 const total = this._statItemsShared + this._statItemsUnshared;
405 if (total === 0) return undefined;
406 return `${
407 this._statItemsShared && Math.round((this._statItemsShared * 100) / total)
408 }% (${this._statItemsShared}/${total}) entries shared via ${
409 this._statSharedSnapshots
410 } shared snapshots (${
411 this._statReusedSharedSnapshots + this._statSharedSnapshots
412 } times referenced)`;
413 }
414
415 storeUnsharedSnapshot(snapshot, locations) {
416 if (locations === undefined) return;
417 const optimizationEntry = {
418 snapshot,
419 shared: 0,
420 snapshotContent: undefined,
421 children: undefined
422 };
423 for (const path of locations) {
424 this._map.set(path, optimizationEntry);
425 }
426 }
427
428 optimize(capturedFiles, startTime, children) {
429 /** @type {Set<string>} */
430 const unsetOptimizationEntries = new Set();
431 /** @type {Set<SnapshotOptimizationEntry>} */
432 const checkedOptimizationEntries = new Set();
433 /**
434 * @param {SnapshotOptimizationEntry} entry optimization entry
435 * @returns {void}
436 */
437 const increaseSharedAndStoreOptimizationEntry = entry => {
438 if (entry.children !== undefined) {
439 entry.children.forEach(increaseSharedAndStoreOptimizationEntry);
440 }
441 entry.shared++;
442 storeOptimizationEntry(entry);
443 };
444 /**
445 * @param {SnapshotOptimizationEntry} entry optimization entry
446 * @returns {void}
447 */
448 const storeOptimizationEntry = entry => {
449 for (const path of entry.snapshotContent) {
450 const old = this._map.get(path);
451 if (old.shared < entry.shared) {
452 this._map.set(path, entry);
453 }
454 capturedFiles.delete(path);
455 }
456 };
457 const capturedFilesSize = capturedFiles.size;
458 capturedFiles: for (const path of capturedFiles) {
459 const optimizationEntry = this._map.get(path);
460 if (optimizationEntry === undefined) {
461 unsetOptimizationEntries.add(path);
462 continue;
463 }
464 if (checkedOptimizationEntries.has(optimizationEntry)) continue;
465 const snapshot = optimizationEntry.snapshot;
466 if (optimizationEntry.shared > 0) {
467 // It's a shared snapshot
468 // We can't change it, so we can only use it when all files match
469 // and startTime is compatible
470 if (
471 startTime &&
472 (!snapshot.startTime || snapshot.startTime > startTime)
473 ) {
474 continue;
475 }
476 const nonSharedFiles = new Set();
477 const snapshotContent = optimizationEntry.snapshotContent;
478 const snapshotEntries = this._get(snapshot);
479 for (const path of snapshotContent) {
480 if (!capturedFiles.has(path)) {
481 if (!snapshotEntries.has(path)) {
482 // File is not shared and can't be removed from the snapshot
483 // because it's in a child of the snapshot
484 checkedOptimizationEntries.add(optimizationEntry);
485 continue capturedFiles;
486 }
487 nonSharedFiles.add(path);
488 continue;
489 }
490 }
491 if (nonSharedFiles.size === 0) {
492 // The complete snapshot is shared
493 // add it as child
494 children.add(snapshot);
495 increaseSharedAndStoreOptimizationEntry(optimizationEntry);
496 this._statReusedSharedSnapshots++;
497 } else {
498 // Only a part of the snapshot is shared
499 const sharedCount = snapshotContent.size - nonSharedFiles.size;
500 if (sharedCount < MIN_COMMON_SNAPSHOT_SIZE) {
501 // Common part it too small
502 checkedOptimizationEntries.add(optimizationEntry);
503 continue capturedFiles;
504 }
505 // Extract common timestamps from both snapshots
506 let commonMap;
507 if (this._isSet) {
508 commonMap = new Set();
509 for (const path of /** @type {Set<string>} */ (snapshotEntries)) {
510 if (nonSharedFiles.has(path)) continue;
511 commonMap.add(path);
512 snapshotEntries.delete(path);
513 }
514 } else {
515 commonMap = new Map();
516 const map = /** @type {Map<string, T>} */ (snapshotEntries);
517 for (const [path, value] of map) {
518 if (nonSharedFiles.has(path)) continue;
519 commonMap.set(path, value);
520 snapshotEntries.delete(path);
521 }
522 }
523 // Create and attach snapshot
524 const commonSnapshot = new Snapshot();
525 commonSnapshot.setMergedStartTime(startTime, snapshot);
526 this._set(commonSnapshot, commonMap);
527 children.add(commonSnapshot);
528 snapshot.addChild(commonSnapshot);
529 // Create optimization entry
530 const newEntry = {
531 snapshot: commonSnapshot,
532 shared: optimizationEntry.shared + 1,
533 snapshotContent: new Set(commonMap.keys()),
534 children: undefined
535 };
536 if (optimizationEntry.children === undefined)
537 optimizationEntry.children = new Set();
538 optimizationEntry.children.add(newEntry);
539 storeOptimizationEntry(newEntry);
540 this._statSharedSnapshots++;
541 }
542 } else {
543 // It's a unshared snapshot
544 // We can extract a common shared snapshot
545 // with all common files
546 const snapshotEntries = this._get(snapshot);
547 let commonMap;
548 if (this._isSet) {
549 commonMap = new Set();
550 const set = /** @type {Set<string>} */ (snapshotEntries);
551 if (capturedFiles.size < set.size) {
552 for (const path of capturedFiles) {
553 if (set.has(path)) commonMap.add(path);
554 }
555 } else {
556 for (const path of set) {
557 if (capturedFiles.has(path)) commonMap.add(path);
558 }
559 }
560 } else {
561 commonMap = new Map();
562 const map = /** @type {Map<string, T>} */ (snapshotEntries);
563 for (const path of capturedFiles) {
564 const ts = map.get(path);
565 if (ts === undefined) continue;
566 commonMap.set(path, ts);
567 }
568 }
569
570 if (commonMap.size < MIN_COMMON_SNAPSHOT_SIZE) {
571 // Common part it too small
572 checkedOptimizationEntries.add(optimizationEntry);
573 continue capturedFiles;
574 }
575 // Create and attach snapshot
576 const commonSnapshot = new Snapshot();
577 commonSnapshot.setMergedStartTime(startTime, snapshot);
578 this._set(commonSnapshot, commonMap);
579 children.add(commonSnapshot);
580 snapshot.addChild(commonSnapshot);
581 // Remove files from snapshot
582 for (const path of commonMap.keys()) snapshotEntries.delete(path);
583 const sharedCount = commonMap.size;
584 this._statItemsUnshared -= sharedCount;
585 this._statItemsShared += sharedCount;
586 // Create optimization entry
587 storeOptimizationEntry({
588 snapshot: commonSnapshot,
589 shared: 2,
590 snapshotContent: new Set(commonMap.keys()),
591 children: undefined
592 });
593 this._statSharedSnapshots++;
594 }
595 checkedOptimizationEntries.add(optimizationEntry);
596 }
597 const unshared = capturedFiles.size;
598 this._statItemsUnshared += unshared;
599 this._statItemsShared += capturedFilesSize - unshared;
600 return unsetOptimizationEntries;
601 }
602}
603
604/* istanbul ignore next */
605/**
606 * @param {number} mtime mtime
607 */
608const applyMtime = mtime => {
609 if (FS_ACCURACY > 1 && mtime % 2 !== 0) FS_ACCURACY = 1;
610 else if (FS_ACCURACY > 10 && mtime % 20 !== 0) FS_ACCURACY = 10;
611 else if (FS_ACCURACY > 100 && mtime % 200 !== 0) FS_ACCURACY = 100;
612 else if (FS_ACCURACY > 1000 && mtime % 2000 !== 0) FS_ACCURACY = 1000;
613};
614
615/**
616 * @template T
617 * @template K
618 * @param {Map<T, K>} a source map
619 * @param {Map<T, K>} b joining map
620 * @returns {Map<T, K>} joined map
621 */
622const mergeMaps = (a, b) => {
623 if (!b || b.size === 0) return a;
624 if (!a || a.size === 0) return b;
625 const map = new Map(a);
626 for (const [key, value] of b) {
627 map.set(key, value);
628 }
629 return map;
630};
631
632/**
633 * @template T
634 * @template K
635 * @param {Set<T, K>} a source map
636 * @param {Set<T, K>} b joining map
637 * @returns {Set<T, K>} joined map
638 */
639const mergeSets = (a, b) => {
640 if (!b || b.size === 0) return a;
641 if (!a || a.size === 0) return b;
642 const map = new Set(a);
643 for (const item of b) {
644 map.add(item);
645 }
646 return map;
647};
648
649/**
650 * Finding file or directory to manage
651 * @param {string} managedPath path that is managing by {@link FileSystemInfo}
652 * @param {string} path path to file or directory
653 * @returns {string|null} managed item
654 * @example
655 * getManagedItem(
656 * '/Users/user/my-project/node_modules/',
657 * '/Users/user/my-project/node_modules/package/index.js'
658 * ) === '/Users/user/my-project/node_modules/package'
659 * getManagedItem(
660 * '/Users/user/my-project/node_modules/',
661 * '/Users/user/my-project/node_modules/package1/node_modules/package2'
662 * ) === '/Users/user/my-project/node_modules/package1/node_modules/package2'
663 * getManagedItem(
664 * '/Users/user/my-project/node_modules/',
665 * '/Users/user/my-project/node_modules/.bin/script.js'
666 * ) === null // hidden files are disallowed as managed items
667 * getManagedItem(
668 * '/Users/user/my-project/node_modules/',
669 * '/Users/user/my-project/node_modules/package'
670 * ) === '/Users/user/my-project/node_modules/package'
671 */
672const getManagedItem = (managedPath, path) => {
673 let i = managedPath.length;
674 let slashes = 1;
675 let startingPosition = true;
676 loop: while (i < path.length) {
677 switch (path.charCodeAt(i)) {
678 case 47: // slash
679 case 92: // backslash
680 if (--slashes === 0) break loop;
681 startingPosition = true;
682 break;
683 case 46: // .
684 // hidden files are disallowed as managed items
685 // it's probably .yarn-integrity or .cache
686 if (startingPosition) return null;
687 break;
688 case 64: // @
689 if (!startingPosition) return null;
690 slashes++;
691 break;
692 default:
693 startingPosition = false;
694 break;
695 }
696 i++;
697 }
698 if (i === path.length) slashes--;
699 // return null when path is incomplete
700 if (slashes !== 0) return null;
701 // if (path.slice(i + 1, i + 13) === "node_modules")
702 if (
703 path.length >= i + 13 &&
704 path.charCodeAt(i + 1) === 110 &&
705 path.charCodeAt(i + 2) === 111 &&
706 path.charCodeAt(i + 3) === 100 &&
707 path.charCodeAt(i + 4) === 101 &&
708 path.charCodeAt(i + 5) === 95 &&
709 path.charCodeAt(i + 6) === 109 &&
710 path.charCodeAt(i + 7) === 111 &&
711 path.charCodeAt(i + 8) === 100 &&
712 path.charCodeAt(i + 9) === 117 &&
713 path.charCodeAt(i + 10) === 108 &&
714 path.charCodeAt(i + 11) === 101 &&
715 path.charCodeAt(i + 12) === 115
716 ) {
717 // if this is the end of the path
718 if (path.length === i + 13) {
719 // return the node_modules directory
720 // it's special
721 return path;
722 }
723 const c = path.charCodeAt(i + 13);
724 // if next symbol is slash or backslash
725 if (c === 47 || c === 92) {
726 // Managed subpath
727 return getManagedItem(path.slice(0, i + 14), path);
728 }
729 }
730 return path.slice(0, i);
731};
732
733/**
734 * @param {FileSystemInfoEntry} entry file system info entry
735 * @returns {boolean} existence flag
736 */
737const toExistence = entry => {
738 return Boolean(entry);
739};
740
741/**
742 * Used to access information about the filesystem in a cached way
743 */
744class FileSystemInfo {
745 /**
746 * @param {InputFileSystem} fs file system
747 * @param {Object} options options
748 * @param {Iterable<string>=} options.managedPaths paths that are only managed by a package manager
749 * @param {Iterable<string>=} options.immutablePaths paths that are immutable
750 * @param {Logger=} options.logger logger used to log invalid snapshots
751 */
752 constructor(fs, { managedPaths = [], immutablePaths = [], logger } = {}) {
753 this.fs = fs;
754 this.logger = logger;
755 this._remainingLogs = logger ? 40 : 0;
756 this._loggedPaths = logger ? new Set() : undefined;
757 /** @type {WeakMap<Snapshot, boolean | (function(WebpackError=, boolean=): void)[]>} */
758 this._snapshotCache = new WeakMap();
759 this._fileTimestampsOptimization = new SnapshotOptimization(
760 s => s.hasFileTimestamps(),
761 s => s.fileTimestamps,
762 (s, v) => s.setFileTimestamps(v)
763 );
764 this._fileHashesOptimization = new SnapshotOptimization(
765 s => s.hasFileHashes(),
766 s => s.fileHashes,
767 (s, v) => s.setFileHashes(v)
768 );
769 this._fileTshsOptimization = new SnapshotOptimization(
770 s => s.hasFileTshs(),
771 s => s.fileTshs,
772 (s, v) => s.setFileTshs(v)
773 );
774 this._contextTimestampsOptimization = new SnapshotOptimization(
775 s => s.hasContextTimestamps(),
776 s => s.contextTimestamps,
777 (s, v) => s.setContextTimestamps(v)
778 );
779 this._contextHashesOptimization = new SnapshotOptimization(
780 s => s.hasContextHashes(),
781 s => s.contextHashes,
782 (s, v) => s.setContextHashes(v)
783 );
784 this._contextTshsOptimization = new SnapshotOptimization(
785 s => s.hasContextTshs(),
786 s => s.contextTshs,
787 (s, v) => s.setContextTshs(v)
788 );
789 this._missingExistenceOptimization = new SnapshotOptimization(
790 s => s.hasMissingExistence(),
791 s => s.missingExistence,
792 (s, v) => s.setMissingExistence(v)
793 );
794 this._managedItemInfoOptimization = new SnapshotOptimization(
795 s => s.hasManagedItemInfo(),
796 s => s.managedItemInfo,
797 (s, v) => s.setManagedItemInfo(v)
798 );
799 this._managedFilesOptimization = new SnapshotOptimization(
800 s => s.hasManagedFiles(),
801 s => s.managedFiles,
802 (s, v) => s.setManagedFiles(v),
803 true
804 );
805 this._managedContextsOptimization = new SnapshotOptimization(
806 s => s.hasManagedContexts(),
807 s => s.managedContexts,
808 (s, v) => s.setManagedContexts(v),
809 true
810 );
811 this._managedMissingOptimization = new SnapshotOptimization(
812 s => s.hasManagedMissing(),
813 s => s.managedMissing,
814 (s, v) => s.setManagedMissing(v),
815 true
816 );
817 /** @type {Map<string, FileSystemInfoEntry | "ignore" | null>} */
818 this._fileTimestamps = new Map();
819 /** @type {Map<string, string>} */
820 this._fileHashes = new Map();
821 /** @type {Map<string, TimestampAndHash | string>} */
822 this._fileTshs = new Map();
823 /** @type {Map<string, FileSystemInfoEntry | "ignore" | null>} */
824 this._contextTimestamps = new Map();
825 /** @type {Map<string, string>} */
826 this._contextHashes = new Map();
827 /** @type {Map<string, TimestampAndHash | string>} */
828 this._contextTshs = new Map();
829 /** @type {Map<string, string>} */
830 this._managedItems = new Map();
831 /** @type {AsyncQueue<string, string, FileSystemInfoEntry | null>} */
832 this.fileTimestampQueue = new AsyncQueue({
833 name: "file timestamp",
834 parallelism: 30,
835 processor: this._readFileTimestamp.bind(this)
836 });
837 /** @type {AsyncQueue<string, string, string | null>} */
838 this.fileHashQueue = new AsyncQueue({
839 name: "file hash",
840 parallelism: 10,
841 processor: this._readFileHash.bind(this)
842 });
843 /** @type {AsyncQueue<string, string, FileSystemInfoEntry | null>} */
844 this.contextTimestampQueue = new AsyncQueue({
845 name: "context timestamp",
846 parallelism: 2,
847 processor: this._readContextTimestamp.bind(this)
848 });
849 /** @type {AsyncQueue<string, string, string | null>} */
850 this.contextHashQueue = new AsyncQueue({
851 name: "context hash",
852 parallelism: 2,
853 processor: this._readContextHash.bind(this)
854 });
855 /** @type {AsyncQueue<string, string, string | null>} */
856 this.managedItemQueue = new AsyncQueue({
857 name: "managed item info",
858 parallelism: 10,
859 processor: this._getManagedItemInfo.bind(this)
860 });
861 /** @type {AsyncQueue<string, string, Set<string>>} */
862 this.managedItemDirectoryQueue = new AsyncQueue({
863 name: "managed item directory info",
864 parallelism: 10,
865 processor: this._getManagedItemDirectoryInfo.bind(this)
866 });
867 this.managedPaths = Array.from(managedPaths);
868 this.managedPathsWithSlash = this.managedPaths.map(p =>
869 join(fs, p, "_").slice(0, -1)
870 );
871 this.immutablePaths = Array.from(immutablePaths);
872 this.immutablePathsWithSlash = this.immutablePaths.map(p =>
873 join(fs, p, "_").slice(0, -1)
874 );
875
876 this._cachedDeprecatedFileTimestamps = undefined;
877 this._cachedDeprecatedContextTimestamps = undefined;
878
879 this._warnAboutExperimentalEsmTracking = false;
880
881 this._statCreatedSnapshots = 0;
882 this._statTestedSnapshotsCached = 0;
883 this._statTestedSnapshotsNotCached = 0;
884 this._statTestedChildrenCached = 0;
885 this._statTestedChildrenNotCached = 0;
886 this._statTestedEntries = 0;
887 }
888
889 logStatistics() {
890 const logWhenMessage = (header, message) => {
891 if (message) {
892 this.logger.log(`${header}: ${message}`);
893 }
894 };
895 this.logger.log(`${this._statCreatedSnapshots} new snapshots created`);
896 this.logger.log(
897 `${
898 this._statTestedSnapshotsNotCached &&
899 Math.round(
900 (this._statTestedSnapshotsNotCached * 100) /
901 (this._statTestedSnapshotsCached +
902 this._statTestedSnapshotsNotCached)
903 )
904 }% root snapshot uncached (${this._statTestedSnapshotsNotCached} / ${
905 this._statTestedSnapshotsCached + this._statTestedSnapshotsNotCached
906 })`
907 );
908 this.logger.log(
909 `${
910 this._statTestedChildrenNotCached &&
911 Math.round(
912 (this._statTestedChildrenNotCached * 100) /
913 (this._statTestedChildrenCached + this._statTestedChildrenNotCached)
914 )
915 }% children snapshot uncached (${this._statTestedChildrenNotCached} / ${
916 this._statTestedChildrenCached + this._statTestedChildrenNotCached
917 })`
918 );
919 this.logger.log(`${this._statTestedEntries} entries tested`);
920 this.logger.log(
921 `File info in cache: ${this._fileTimestamps.size} timestamps ${this._fileHashes.size} hashes ${this._fileTshs.size} timestamp hash combinations`
922 );
923 logWhenMessage(
924 `File timestamp snapshot optimization`,
925 this._fileTimestampsOptimization.getStatisticMessage()
926 );
927 logWhenMessage(
928 `File hash snapshot optimization`,
929 this._fileHashesOptimization.getStatisticMessage()
930 );
931 logWhenMessage(
932 `File timestamp hash combination snapshot optimization`,
933 this._fileTshsOptimization.getStatisticMessage()
934 );
935 this.logger.log(
936 `Directory info in cache: ${this._contextTimestamps.size} timestamps ${this._contextHashes.size} hashes ${this._contextTshs.size} timestamp hash combinations`
937 );
938 logWhenMessage(
939 `Directory timestamp snapshot optimization`,
940 this._contextTimestampsOptimization.getStatisticMessage()
941 );
942 logWhenMessage(
943 `Directory hash snapshot optimization`,
944 this._contextHashesOptimization.getStatisticMessage()
945 );
946 logWhenMessage(
947 `Directory timestamp hash combination snapshot optimization`,
948 this._contextTshsOptimization.getStatisticMessage()
949 );
950 logWhenMessage(
951 `Missing items snapshot optimization`,
952 this._missingExistenceOptimization.getStatisticMessage()
953 );
954 this.logger.log(
955 `Managed items info in cache: ${this._managedItems.size} items`
956 );
957 logWhenMessage(
958 `Managed items snapshot optimization`,
959 this._managedItemInfoOptimization.getStatisticMessage()
960 );
961 logWhenMessage(
962 `Managed files snapshot optimization`,
963 this._managedFilesOptimization.getStatisticMessage()
964 );
965 logWhenMessage(
966 `Managed contexts snapshot optimization`,
967 this._managedContextsOptimization.getStatisticMessage()
968 );
969 logWhenMessage(
970 `Managed missing snapshot optimization`,
971 this._managedMissingOptimization.getStatisticMessage()
972 );
973 }
974
975 _log(path, reason, ...args) {
976 const key = path + reason;
977 if (this._loggedPaths.has(key)) return;
978 this._loggedPaths.add(key);
979 this.logger.debug(`${path} invalidated because ${reason}`, ...args);
980 if (--this._remainingLogs === 0) {
981 this.logger.debug(
982 "Logging limit has been reached and no further logging will be emitted by FileSystemInfo"
983 );
984 }
985 }
986
987 /**
988 * @param {Map<string, FileSystemInfoEntry | "ignore" | null>} map timestamps
989 * @returns {void}
990 */
991 addFileTimestamps(map) {
992 for (const [path, ts] of map) {
993 this._fileTimestamps.set(path, ts);
994 }
995 this._cachedDeprecatedFileTimestamps = undefined;
996 }
997
998 /**
999 * @param {Map<string, FileSystemInfoEntry | "ignore" | null>} map timestamps
1000 * @returns {void}
1001 */
1002 addContextTimestamps(map) {
1003 for (const [path, ts] of map) {
1004 this._contextTimestamps.set(path, ts);
1005 }
1006 this._cachedDeprecatedContextTimestamps = undefined;
1007 }
1008
1009 /**
1010 * @param {string} path file path
1011 * @param {function(WebpackError=, (FileSystemInfoEntry | "ignore" | null)=): void} callback callback function
1012 * @returns {void}
1013 */
1014 getFileTimestamp(path, callback) {
1015 const cache = this._fileTimestamps.get(path);
1016 if (cache !== undefined) return callback(null, cache);
1017 this.fileTimestampQueue.add(path, callback);
1018 }
1019
1020 /**
1021 * @param {string} path context path
1022 * @param {function(WebpackError=, (FileSystemInfoEntry | "ignore" | null)=): void} callback callback function
1023 * @returns {void}
1024 */
1025 getContextTimestamp(path, callback) {
1026 const cache = this._contextTimestamps.get(path);
1027 if (cache !== undefined) return callback(null, cache);
1028 this.contextTimestampQueue.add(path, callback);
1029 }
1030
1031 /**
1032 * @param {string} path file path
1033 * @param {function(WebpackError=, string=): void} callback callback function
1034 * @returns {void}
1035 */
1036 getFileHash(path, callback) {
1037 const cache = this._fileHashes.get(path);
1038 if (cache !== undefined) return callback(null, cache);
1039 this.fileHashQueue.add(path, callback);
1040 }
1041
1042 /**
1043 * @param {string} path context path
1044 * @param {function(WebpackError=, string=): void} callback callback function
1045 * @returns {void}
1046 */
1047 getContextHash(path, callback) {
1048 const cache = this._contextHashes.get(path);
1049 if (cache !== undefined) return callback(null, cache);
1050 this.contextHashQueue.add(path, callback);
1051 }
1052
1053 _createBuildDependenciesResolvers() {
1054 const resolveContext = createResolver({
1055 resolveToContext: true,
1056 exportsFields: [],
1057 fileSystem: this.fs
1058 });
1059 const resolveCjs = createResolver({
1060 extensions: [".js", ".json", ".node"],
1061 conditionNames: ["require", "node"],
1062 fileSystem: this.fs
1063 });
1064 const resolveEsm = createResolver({
1065 extensions: [".js", ".json", ".node"],
1066 fullySpecified: true,
1067 conditionNames: ["import", "node"],
1068 fileSystem: this.fs
1069 });
1070 return { resolveContext, resolveEsm, resolveCjs };
1071 }
1072
1073 /**
1074 * @param {string} context context directory
1075 * @param {Iterable<string>} deps dependencies
1076 * @param {function(Error=, ResolveBuildDependenciesResult=): void} callback callback function
1077 * @returns {void}
1078 */
1079 resolveBuildDependencies(context, deps, callback) {
1080 const {
1081 resolveContext,
1082 resolveEsm,
1083 resolveCjs
1084 } = this._createBuildDependenciesResolvers();
1085
1086 /** @type {Set<string>} */
1087 const files = new Set();
1088 /** @type {Set<string>} */
1089 const fileSymlinks = new Set();
1090 /** @type {Set<string>} */
1091 const directories = new Set();
1092 /** @type {Set<string>} */
1093 const directorySymlinks = new Set();
1094 /** @type {Set<string>} */
1095 const missing = new Set();
1096 /** @type {Set<string>} */
1097 const resolveFiles = new Set();
1098 /** @type {Set<string>} */
1099 const resolveDirectories = new Set();
1100 /** @type {Set<string>} */
1101 const resolveMissing = new Set();
1102 /** @type {Map<string, string>} */
1103 const resolveResults = new Map();
1104 const invalidResolveResults = new Set();
1105 const resolverContext = {
1106 fileDependencies: resolveFiles,
1107 contextDependencies: resolveDirectories,
1108 missingDependencies: resolveMissing
1109 };
1110 const expectedToString = expected => {
1111 return expected ? ` (expected ${expected})` : "";
1112 };
1113 const jobToString = job => {
1114 switch (job.type) {
1115 case RBDT_RESOLVE_CJS:
1116 return `resolve commonjs ${job.path}${expectedToString(
1117 job.expected
1118 )}`;
1119 case RBDT_RESOLVE_ESM:
1120 return `resolve esm ${job.path}${expectedToString(job.expected)}`;
1121 case RBDT_RESOLVE_DIRECTORY:
1122 return `resolve directory ${job.path}`;
1123 case RBDT_RESOLVE_CJS_FILE:
1124 return `resolve commonjs file ${job.path}${expectedToString(
1125 job.expected
1126 )}`;
1127 case RBDT_RESOLVE_ESM_FILE:
1128 return `resolve esm file ${job.path}${expectedToString(
1129 job.expected
1130 )}`;
1131 case RBDT_DIRECTORY:
1132 return `directory ${job.path}`;
1133 case RBDT_FILE:
1134 return `file ${job.path}`;
1135 case RBDT_DIRECTORY_DEPENDENCIES:
1136 return `directory dependencies ${job.path}`;
1137 case RBDT_FILE_DEPENDENCIES:
1138 return `file dependencies ${job.path}`;
1139 }
1140 return `unknown ${job.type} ${job.path}`;
1141 };
1142 const pathToString = job => {
1143 let result = ` at ${jobToString(job)}`;
1144 job = job.issuer;
1145 while (job !== undefined) {
1146 result += `\n at ${jobToString(job)}`;
1147 job = job.issuer;
1148 }
1149 return result;
1150 };
1151 processAsyncTree(
1152 Array.from(deps, dep => ({
1153 type: RBDT_RESOLVE_CJS,
1154 context,
1155 path: dep,
1156 expected: undefined,
1157 issuer: undefined
1158 })),
1159 20,
1160 (job, push, callback) => {
1161 const { type, context, path, expected } = job;
1162 const resolveDirectory = path => {
1163 const key = `d\n${context}\n${path}`;
1164 if (resolveResults.has(key)) {
1165 return callback();
1166 }
1167 resolveResults.set(key, undefined);
1168 resolveContext(context, path, resolverContext, (err, result) => {
1169 if (err) {
1170 invalidResolveResults.add(key);
1171 if (
1172 err.code === "ENOENT" ||
1173 err.code === "UNDECLARED_DEPENDENCY"
1174 ) {
1175 return callback();
1176 }
1177 err.message += `\nwhile resolving '${path}' in ${context} to a directory`;
1178 return callback(err);
1179 }
1180 resolveResults.set(key, result);
1181 push({
1182 type: RBDT_DIRECTORY,
1183 context: undefined,
1184 path: result,
1185 expected: undefined,
1186 issuer: job
1187 });
1188 callback();
1189 });
1190 };
1191 const resolveFile = (path, symbol, resolve) => {
1192 const key = `${symbol}\n${context}\n${path}`;
1193 if (resolveResults.has(key)) {
1194 return callback();
1195 }
1196 resolveResults.set(key, undefined);
1197 resolve(context, path, resolverContext, (err, result) => {
1198 if (expected) {
1199 if (result === expected) {
1200 resolveResults.set(key, result);
1201 } else {
1202 invalidResolveResults.add(key);
1203 this.logger.debug(
1204 `Resolving '${path}' in ${context} for build dependencies doesn't lead to expected result '${expected}', but to '${result}' instead. Resolving dependencies are ignored for this path.\n${pathToString(
1205 job
1206 )}`
1207 );
1208 }
1209 } else {
1210 if (err) {
1211 invalidResolveResults.add(key);
1212 if (
1213 err.code === "ENOENT" ||
1214 err.code === "UNDECLARED_DEPENDENCY"
1215 ) {
1216 return callback();
1217 }
1218 err.message += `\nwhile resolving '${path}' in ${context} as file\n${pathToString(
1219 job
1220 )}`;
1221 return callback(err);
1222 }
1223 resolveResults.set(key, result);
1224 push({
1225 type: RBDT_FILE,
1226 context: undefined,
1227 path: result,
1228 expected: undefined,
1229 issuer: job
1230 });
1231 }
1232 callback();
1233 });
1234 };
1235 switch (type) {
1236 case RBDT_RESOLVE_CJS: {
1237 const isDirectory = /[\\/]$/.test(path);
1238 if (isDirectory) {
1239 resolveDirectory(path.slice(0, path.length - 1));
1240 } else {
1241 resolveFile(path, "f", resolveCjs);
1242 }
1243 break;
1244 }
1245 case RBDT_RESOLVE_ESM: {
1246 const isDirectory = /[\\/]$/.test(path);
1247 if (isDirectory) {
1248 resolveDirectory(path.slice(0, path.length - 1));
1249 } else {
1250 resolveFile(path);
1251 }
1252 break;
1253 }
1254 case RBDT_RESOLVE_DIRECTORY: {
1255 resolveDirectory(path);
1256 break;
1257 }
1258 case RBDT_RESOLVE_CJS_FILE: {
1259 resolveFile(path, "f", resolveCjs);
1260 break;
1261 }
1262 case RBDT_RESOLVE_ESM_FILE: {
1263 resolveFile(path, "e", resolveEsm);
1264 break;
1265 }
1266 case RBDT_FILE: {
1267 if (files.has(path)) {
1268 callback();
1269 break;
1270 }
1271 files.add(path);
1272 this.fs.realpath(path, (err, _realPath) => {
1273 if (err) return callback(err);
1274 const realPath = /** @type {string} */ (_realPath);
1275 if (realPath !== path) {
1276 fileSymlinks.add(path);
1277 resolveFiles.add(path);
1278 if (files.has(realPath)) return callback();
1279 files.add(realPath);
1280 }
1281 push({
1282 type: RBDT_FILE_DEPENDENCIES,
1283 context: undefined,
1284 path: realPath,
1285 expected: undefined,
1286 issuer: job
1287 });
1288 callback();
1289 });
1290 break;
1291 }
1292 case RBDT_DIRECTORY: {
1293 if (directories.has(path)) {
1294 callback();
1295 break;
1296 }
1297 directories.add(path);
1298 this.fs.realpath(path, (err, _realPath) => {
1299 if (err) return callback(err);
1300 const realPath = /** @type {string} */ (_realPath);
1301 if (realPath !== path) {
1302 directorySymlinks.add(path);
1303 resolveFiles.add(path);
1304 if (directories.has(realPath)) return callback();
1305 directories.add(realPath);
1306 }
1307 push({
1308 type: RBDT_DIRECTORY_DEPENDENCIES,
1309 context: undefined,
1310 path: realPath,
1311 expected: undefined,
1312 issuer: job
1313 });
1314 callback();
1315 });
1316 break;
1317 }
1318 case RBDT_FILE_DEPENDENCIES: {
1319 // Check for known files without dependencies
1320 if (/\.json5?$|\.yarn-integrity$|yarn\.lock$|\.ya?ml/.test(path)) {
1321 process.nextTick(callback);
1322 break;
1323 }
1324 // Check commonjs cache for the module
1325 /** @type {NodeModule} */
1326 const module = require.cache[path];
1327 if (module && Array.isArray(module.children)) {
1328 children: for (const child of module.children) {
1329 let childPath = child.filename;
1330 if (childPath) {
1331 push({
1332 type: RBDT_FILE,
1333 context: undefined,
1334 path: childPath,
1335 expected: undefined,
1336 issuer: job
1337 });
1338 const context = dirname(this.fs, path);
1339 for (const modulePath of module.paths) {
1340 if (childPath.startsWith(modulePath)) {
1341 let request = childPath.slice(modulePath.length + 1);
1342 if (request.endsWith(".js"))
1343 request = request.slice(0, -3);
1344 push({
1345 type: RBDT_RESOLVE_CJS_FILE,
1346 context,
1347 path: request,
1348 expected: child.filename,
1349 issuer: job
1350 });
1351 continue children;
1352 }
1353 }
1354 let request = relative(this.fs, context, childPath);
1355 if (request.endsWith(".js")) request = request.slice(0, -3);
1356 request = request.replace(/\\/g, "/");
1357 if (!request.startsWith("../")) request = `./${request}`;
1358 push({
1359 type: RBDT_RESOLVE_CJS_FILE,
1360 context,
1361 path: request,
1362 expected: child.filename,
1363 issuer: job
1364 });
1365 }
1366 }
1367 } else if (supportsEsm && /\.m?js$/.test(path)) {
1368 if (!this._warnAboutExperimentalEsmTracking) {
1369 this.logger.info(
1370 "Node.js doesn't offer a (nice) way to introspect the ESM dependency graph yet.\n" +
1371 "Until a full solution is available webpack uses an experimental ESM tracking based on parsing.\n" +
1372 "As best effort webpack parses the ESM files to guess dependencies. But this can lead to expensive and incorrect tracking."
1373 );
1374 this._warnAboutExperimentalEsmTracking = true;
1375 }
1376 const lexer = require("es-module-lexer");
1377 lexer.init.then(() => {
1378 this.fs.readFile(path, (err, content) => {
1379 if (err) return callback(err);
1380 try {
1381 const context = dirname(this.fs, path);
1382 const source = content.toString();
1383 const [imports] = lexer.parse(source);
1384 for (const imp of imports) {
1385 try {
1386 let dependency;
1387 if (imp.d === -1) {
1388 // import ... from "..."
1389 dependency = JSON.parse(
1390 source.substring(imp.s - 1, imp.e + 1)
1391 );
1392 } else if (imp.d > -1) {
1393 // import()
1394 let expr = source.substring(imp.s, imp.e).trim();
1395 if (expr[0] === "'")
1396 expr = `"${expr
1397 .slice(1, -1)
1398 .replace(/"/g, '\\"')}"`;
1399 dependency = JSON.parse(expr);
1400 } else {
1401 // e.g. import.meta
1402 continue;
1403 }
1404 push({
1405 type: RBDT_RESOLVE_ESM_FILE,
1406 context,
1407 path: dependency,
1408 expected: undefined,
1409 issuer: job
1410 });
1411 } catch (e) {
1412 this.logger.warn(
1413 `Parsing of ${path} for build dependencies failed at 'import(${source.substring(
1414 imp.s,
1415 imp.e
1416 )})'.\n` +
1417 "Build dependencies behind this expression are ignored and might cause incorrect cache invalidation."
1418 );
1419 this.logger.debug(pathToString(job));
1420 this.logger.debug(e.stack);
1421 }
1422 }
1423 } catch (e) {
1424 this.logger.warn(
1425 `Parsing of ${path} for build dependencies failed and all dependencies of this file are ignored, which might cause incorrect cache invalidation..`
1426 );
1427 this.logger.debug(pathToString(job));
1428 this.logger.debug(e.stack);
1429 }
1430 process.nextTick(callback);
1431 });
1432 }, callback);
1433 break;
1434 } else {
1435 this.logger.log(
1436 `Assuming ${path} has no dependencies as we were unable to assign it to any module system.`
1437 );
1438 this.logger.debug(pathToString(job));
1439 }
1440 process.nextTick(callback);
1441 break;
1442 }
1443 case RBDT_DIRECTORY_DEPENDENCIES: {
1444 const match = /(^.+[\\/]node_modules[\\/](?:@[^\\/]+[\\/])?[^\\/]+)/.exec(
1445 path
1446 );
1447 const packagePath = match ? match[1] : path;
1448 const packageJson = join(this.fs, packagePath, "package.json");
1449 this.fs.readFile(packageJson, (err, content) => {
1450 if (err) {
1451 if (err.code === "ENOENT") {
1452 resolveMissing.add(packageJson);
1453 const parent = dirname(this.fs, packagePath);
1454 if (parent !== packagePath) {
1455 push({
1456 type: RBDT_DIRECTORY_DEPENDENCIES,
1457 context: undefined,
1458 path: parent,
1459 expected: undefined,
1460 issuer: job
1461 });
1462 }
1463 callback();
1464 return;
1465 }
1466 return callback(err);
1467 }
1468 resolveFiles.add(packageJson);
1469 let packageData;
1470 try {
1471 packageData = JSON.parse(content.toString("utf-8"));
1472 } catch (e) {
1473 return callback(e);
1474 }
1475 const depsObject = packageData.dependencies;
1476 if (typeof depsObject === "object" && depsObject) {
1477 for (const dep of Object.keys(depsObject)) {
1478 push({
1479 type: RBDT_RESOLVE_DIRECTORY,
1480 context: packagePath,
1481 path: dep,
1482 expected: undefined,
1483 issuer: job
1484 });
1485 }
1486 }
1487 callback();
1488 });
1489 break;
1490 }
1491 }
1492 },
1493 err => {
1494 if (err) return callback(err);
1495 for (const l of fileSymlinks) files.delete(l);
1496 for (const l of directorySymlinks) directories.delete(l);
1497 for (const k of invalidResolveResults) resolveResults.delete(k);
1498 callback(null, {
1499 files,
1500 directories,
1501 missing,
1502 resolveResults,
1503 resolveDependencies: {
1504 files: resolveFiles,
1505 directories: resolveDirectories,
1506 missing: resolveMissing
1507 }
1508 });
1509 }
1510 );
1511 }
1512
1513 /**
1514 * @param {Map<string, string>} resolveResults results from resolving
1515 * @param {function(Error=, boolean=): void} callback callback with true when resolveResults resolve the same way
1516 * @returns {void}
1517 */
1518 checkResolveResultsValid(resolveResults, callback) {
1519 const {
1520 resolveCjs,
1521 resolveEsm,
1522 resolveContext
1523 } = this._createBuildDependenciesResolvers();
1524 asyncLib.eachLimit(
1525 resolveResults,
1526 20,
1527 ([key, expectedResult], callback) => {
1528 const [type, context, path] = key.split("\n");
1529 switch (type) {
1530 case "d":
1531 resolveContext(context, path, {}, (err, result) => {
1532 if (err) return callback(err);
1533 if (result !== expectedResult) return callback(INVALID);
1534 callback();
1535 });
1536 break;
1537 case "f":
1538 resolveCjs(context, path, {}, (err, result) => {
1539 if (err) return callback(err);
1540 if (result !== expectedResult) return callback(INVALID);
1541 callback();
1542 });
1543 break;
1544 case "e":
1545 resolveEsm(context, path, {}, (err, result) => {
1546 if (err) return callback(err);
1547 if (result !== expectedResult) return callback(INVALID);
1548 callback();
1549 });
1550 break;
1551 default:
1552 callback(new Error("Unexpected type in resolve result key"));
1553 break;
1554 }
1555 },
1556 /**
1557 * @param {Error | typeof INVALID=} err error or invalid flag
1558 * @returns {void}
1559 */
1560 err => {
1561 if (err === INVALID) {
1562 return callback(null, false);
1563 }
1564 if (err) {
1565 return callback(err);
1566 }
1567 return callback(null, true);
1568 }
1569 );
1570 }
1571
1572 /**
1573 *
1574 * @param {number} startTime when processing the files has started
1575 * @param {Iterable<string>} files all files
1576 * @param {Iterable<string>} directories all directories
1577 * @param {Iterable<string>} missing all missing files or directories
1578 * @param {Object} options options object (for future extensions)
1579 * @param {boolean=} options.hash should use hash to snapshot
1580 * @param {boolean=} options.timestamp should use timestamp to snapshot
1581 * @param {function(WebpackError=, Snapshot=): void} callback callback function
1582 * @returns {void}
1583 */
1584 createSnapshot(startTime, files, directories, missing, options, callback) {
1585 /** @type {Map<string, FileSystemInfoEntry>} */
1586 const fileTimestamps = new Map();
1587 /** @type {Map<string, string>} */
1588 const fileHashes = new Map();
1589 /** @type {Map<string, TimestampAndHash | string>} */
1590 const fileTshs = new Map();
1591 /** @type {Map<string, FileSystemInfoEntry>} */
1592 const contextTimestamps = new Map();
1593 /** @type {Map<string, string>} */
1594 const contextHashes = new Map();
1595 /** @type {Map<string, TimestampAndHash | string>} */
1596 const contextTshs = new Map();
1597 /** @type {Map<string, boolean>} */
1598 const missingExistence = new Map();
1599 /** @type {Map<string, string>} */
1600 const managedItemInfo = new Map();
1601 /** @type {Set<string>} */
1602 const managedFiles = new Set();
1603 /** @type {Set<string>} */
1604 const managedContexts = new Set();
1605 /** @type {Set<string>} */
1606 const managedMissing = new Set();
1607 /** @type {Set<Snapshot>} */
1608 const children = new Set();
1609
1610 /** @type {Set<string>} */
1611 let unsharedFileTimestamps;
1612 /** @type {Set<string>} */
1613 let unsharedFileHashes;
1614 /** @type {Set<string>} */
1615 let unsharedFileTshs;
1616 /** @type {Set<string>} */
1617 let unsharedContextTimestamps;
1618 /** @type {Set<string>} */
1619 let unsharedContextHashes;
1620 /** @type {Set<string>} */
1621 let unsharedContextTshs;
1622 /** @type {Set<string>} */
1623 let unsharedMissingExistence;
1624 /** @type {Set<string>} */
1625 let unsharedManagedItemInfo;
1626
1627 /** @type {Set<string>} */
1628 const managedItems = new Set();
1629
1630 /** 1 = timestamp, 2 = hash, 3 = timestamp + hash */
1631 const mode = options && options.hash ? (options.timestamp ? 3 : 2) : 1;
1632
1633 let jobs = 1;
1634 const jobDone = () => {
1635 if (--jobs === 0) {
1636 const snapshot = new Snapshot();
1637 if (startTime) snapshot.setStartTime(startTime);
1638 if (fileTimestamps.size !== 0) {
1639 snapshot.setFileTimestamps(fileTimestamps);
1640 this._fileTimestampsOptimization.storeUnsharedSnapshot(
1641 snapshot,
1642 unsharedFileTimestamps
1643 );
1644 }
1645 if (fileHashes.size !== 0) {
1646 snapshot.setFileHashes(fileHashes);
1647 this._fileHashesOptimization.storeUnsharedSnapshot(
1648 snapshot,
1649 unsharedFileHashes
1650 );
1651 }
1652 if (fileTshs.size !== 0) {
1653 snapshot.setFileTshs(fileTshs);
1654 this._fileTshsOptimization.storeUnsharedSnapshot(
1655 snapshot,
1656 unsharedFileTshs
1657 );
1658 }
1659 if (contextTimestamps.size !== 0) {
1660 snapshot.setContextTimestamps(contextTimestamps);
1661 this._contextTimestampsOptimization.storeUnsharedSnapshot(
1662 snapshot,
1663 unsharedContextTimestamps
1664 );
1665 }
1666 if (contextHashes.size !== 0) {
1667 snapshot.setContextHashes(contextHashes);
1668 this._contextHashesOptimization.storeUnsharedSnapshot(
1669 snapshot,
1670 unsharedContextHashes
1671 );
1672 }
1673 if (contextTshs.size !== 0) {
1674 snapshot.setContextTshs(contextTshs);
1675 this._contextTshsOptimization.storeUnsharedSnapshot(
1676 snapshot,
1677 unsharedContextTshs
1678 );
1679 }
1680 if (missingExistence.size !== 0) {
1681 snapshot.setMissingExistence(missingExistence);
1682 this._missingExistenceOptimization.storeUnsharedSnapshot(
1683 snapshot,
1684 unsharedMissingExistence
1685 );
1686 }
1687 if (managedItemInfo.size !== 0) {
1688 snapshot.setManagedItemInfo(managedItemInfo);
1689 this._managedItemInfoOptimization.storeUnsharedSnapshot(
1690 snapshot,
1691 unsharedManagedItemInfo
1692 );
1693 }
1694 const unsharedManagedFiles = this._managedFilesOptimization.optimize(
1695 managedFiles,
1696 undefined,
1697 children
1698 );
1699 if (managedFiles.size !== 0) {
1700 snapshot.setManagedFiles(managedFiles);
1701 this._managedFilesOptimization.storeUnsharedSnapshot(
1702 snapshot,
1703 unsharedManagedFiles
1704 );
1705 }
1706 const unsharedManagedContexts = this._managedContextsOptimization.optimize(
1707 managedContexts,
1708 undefined,
1709 children
1710 );
1711 if (managedContexts.size !== 0) {
1712 snapshot.setManagedContexts(managedContexts);
1713 this._managedContextsOptimization.storeUnsharedSnapshot(
1714 snapshot,
1715 unsharedManagedContexts
1716 );
1717 }
1718 const unsharedManagedMissing = this._managedMissingOptimization.optimize(
1719 managedMissing,
1720 undefined,
1721 children
1722 );
1723 if (managedMissing.size !== 0) {
1724 snapshot.setManagedMissing(managedMissing);
1725 this._managedMissingOptimization.storeUnsharedSnapshot(
1726 snapshot,
1727 unsharedManagedMissing
1728 );
1729 }
1730 if (children.size !== 0) {
1731 snapshot.setChildren(children);
1732 }
1733 this._snapshotCache.set(snapshot, true);
1734 this._statCreatedSnapshots++;
1735
1736 callback(null, snapshot);
1737 }
1738 };
1739 const jobError = () => {
1740 if (jobs > 0) {
1741 // large negative number instead of NaN or something else to keep jobs to stay a SMI (v8)
1742 jobs = -100000000;
1743 callback(null, null);
1744 }
1745 };
1746 const checkManaged = (path, managedSet) => {
1747 for (const immutablePath of this.immutablePathsWithSlash) {
1748 if (path.startsWith(immutablePath)) {
1749 managedSet.add(path);
1750 return true;
1751 }
1752 }
1753 for (const managedPath of this.managedPathsWithSlash) {
1754 if (path.startsWith(managedPath)) {
1755 const managedItem = getManagedItem(managedPath, path);
1756 if (managedItem) {
1757 managedItems.add(managedItem);
1758 managedSet.add(path);
1759 return true;
1760 }
1761 }
1762 }
1763 return false;
1764 };
1765 const captureNonManaged = (items, managedSet) => {
1766 const capturedItems = new Set();
1767 for (const path of items) {
1768 if (!checkManaged(path, managedSet)) capturedItems.add(path);
1769 }
1770 return capturedItems;
1771 };
1772 if (files) {
1773 const capturedFiles = captureNonManaged(files, managedFiles);
1774 switch (mode) {
1775 case 3:
1776 unsharedFileTshs = this._fileTshsOptimization.optimize(
1777 capturedFiles,
1778 undefined,
1779 children
1780 );
1781 for (const path of capturedFiles) {
1782 const cache = this._fileTshs.get(path);
1783 if (cache !== undefined) {
1784 fileTshs.set(path, cache);
1785 } else {
1786 jobs++;
1787 this._getFileTimestampAndHash(path, (err, entry) => {
1788 if (err) {
1789 if (this.logger) {
1790 this.logger.debug(
1791 `Error snapshotting file timestamp hash combination of ${path}: ${err.stack}`
1792 );
1793 }
1794 jobError();
1795 } else {
1796 fileTshs.set(path, entry);
1797 jobDone();
1798 }
1799 });
1800 }
1801 }
1802 break;
1803 case 2:
1804 unsharedFileHashes = this._fileHashesOptimization.optimize(
1805 capturedFiles,
1806 undefined,
1807 children
1808 );
1809 for (const path of capturedFiles) {
1810 const cache = this._fileHashes.get(path);
1811 if (cache !== undefined) {
1812 fileHashes.set(path, cache);
1813 } else {
1814 jobs++;
1815 this.fileHashQueue.add(path, (err, entry) => {
1816 if (err) {
1817 if (this.logger) {
1818 this.logger.debug(
1819 `Error snapshotting file hash of ${path}: ${err.stack}`
1820 );
1821 }
1822 jobError();
1823 } else {
1824 fileHashes.set(path, entry);
1825 jobDone();
1826 }
1827 });
1828 }
1829 }
1830 break;
1831 case 1:
1832 unsharedFileTimestamps = this._fileTimestampsOptimization.optimize(
1833 capturedFiles,
1834 startTime,
1835 children
1836 );
1837 for (const path of capturedFiles) {
1838 const cache = this._fileTimestamps.get(path);
1839 if (cache !== undefined) {
1840 if (cache !== "ignore") {
1841 fileTimestamps.set(path, cache);
1842 }
1843 } else {
1844 jobs++;
1845 this.fileTimestampQueue.add(path, (err, entry) => {
1846 if (err) {
1847 if (this.logger) {
1848 this.logger.debug(
1849 `Error snapshotting file timestamp of ${path}: ${err.stack}`
1850 );
1851 }
1852 jobError();
1853 } else {
1854 fileTimestamps.set(path, entry);
1855 jobDone();
1856 }
1857 });
1858 }
1859 }
1860 break;
1861 }
1862 }
1863 if (directories) {
1864 const capturedDirectories = captureNonManaged(
1865 directories,
1866 managedContexts
1867 );
1868 switch (mode) {
1869 case 3:
1870 unsharedContextTshs = this._contextTshsOptimization.optimize(
1871 capturedDirectories,
1872 undefined,
1873 children
1874 );
1875 for (const path of capturedDirectories) {
1876 const cache = this._contextTshs.get(path);
1877 if (cache !== undefined) {
1878 contextTshs.set(path, cache);
1879 } else {
1880 jobs++;
1881 this._getContextTimestampAndHash(path, (err, entry) => {
1882 if (err) {
1883 if (this.logger) {
1884 this.logger.debug(
1885 `Error snapshotting context timestamp hash combination of ${path}: ${err.stack}`
1886 );
1887 }
1888 jobError();
1889 } else {
1890 contextTshs.set(path, entry);
1891 jobDone();
1892 }
1893 });
1894 }
1895 }
1896 break;
1897 case 2:
1898 unsharedContextHashes = this._contextHashesOptimization.optimize(
1899 capturedDirectories,
1900 undefined,
1901 children
1902 );
1903 for (const path of capturedDirectories) {
1904 const cache = this._contextHashes.get(path);
1905 if (cache !== undefined) {
1906 contextHashes.set(path, cache);
1907 } else {
1908 jobs++;
1909 this.contextHashQueue.add(path, (err, entry) => {
1910 if (err) {
1911 if (this.logger) {
1912 this.logger.debug(
1913 `Error snapshotting context hash of ${path}: ${err.stack}`
1914 );
1915 }
1916 jobError();
1917 } else {
1918 contextHashes.set(path, entry);
1919 jobDone();
1920 }
1921 });
1922 }
1923 }
1924 break;
1925 case 1:
1926 unsharedContextTimestamps = this._contextTimestampsOptimization.optimize(
1927 capturedDirectories,
1928 startTime,
1929 children
1930 );
1931 for (const path of capturedDirectories) {
1932 const cache = this._contextTimestamps.get(path);
1933 if (cache !== undefined) {
1934 if (cache !== "ignore") {
1935 contextTimestamps.set(path, cache);
1936 }
1937 } else {
1938 jobs++;
1939 this.contextTimestampQueue.add(path, (err, entry) => {
1940 if (err) {
1941 if (this.logger) {
1942 this.logger.debug(
1943 `Error snapshotting context timestamp of ${path}: ${err.stack}`
1944 );
1945 }
1946 jobError();
1947 } else {
1948 contextTimestamps.set(path, entry);
1949 jobDone();
1950 }
1951 });
1952 }
1953 }
1954 break;
1955 }
1956 }
1957 if (missing) {
1958 const capturedMissing = captureNonManaged(missing, managedMissing);
1959 unsharedMissingExistence = this._missingExistenceOptimization.optimize(
1960 capturedMissing,
1961 startTime,
1962 children
1963 );
1964 for (const path of capturedMissing) {
1965 const cache = this._fileTimestamps.get(path);
1966 if (cache !== undefined) {
1967 if (cache !== "ignore") {
1968 missingExistence.set(path, toExistence(cache));
1969 }
1970 } else {
1971 jobs++;
1972 this.fileTimestampQueue.add(path, (err, entry) => {
1973 if (err) {
1974 if (this.logger) {
1975 this.logger.debug(
1976 `Error snapshotting missing timestamp of ${path}: ${err.stack}`
1977 );
1978 }
1979 jobError();
1980 } else {
1981 missingExistence.set(path, toExistence(entry));
1982 jobDone();
1983 }
1984 });
1985 }
1986 }
1987 }
1988 unsharedManagedItemInfo = this._managedItemInfoOptimization.optimize(
1989 managedItems,
1990 undefined,
1991 children
1992 );
1993 for (const path of managedItems) {
1994 const cache = this._managedItems.get(path);
1995 if (cache !== undefined) {
1996 managedItemInfo.set(path, cache);
1997 } else {
1998 jobs++;
1999 this.managedItemQueue.add(path, (err, entry) => {
2000 if (err) {
2001 if (this.logger) {
2002 this.logger.debug(
2003 `Error snapshotting managed item ${path}: ${err.stack}`
2004 );
2005 }
2006 jobError();
2007 } else {
2008 managedItemInfo.set(path, entry);
2009 jobDone();
2010 }
2011 });
2012 }
2013 }
2014 jobDone();
2015 }
2016
2017 /**
2018 * @param {Snapshot} snapshot1 a snapshot
2019 * @param {Snapshot} snapshot2 a snapshot
2020 * @returns {Snapshot} merged snapshot
2021 */
2022 mergeSnapshots(snapshot1, snapshot2) {
2023 const snapshot = new Snapshot();
2024 if (snapshot1.hasStartTime() && snapshot2.hasStartTime())
2025 snapshot.setStartTime(Math.min(snapshot1.startTime, snapshot2.startTime));
2026 else if (snapshot2.hasStartTime()) snapshot.startTime = snapshot2.startTime;
2027 else if (snapshot1.hasStartTime()) snapshot.startTime = snapshot1.startTime;
2028 if (snapshot1.hasFileTimestamps() || snapshot2.hasFileTimestamps()) {
2029 snapshot.setFileTimestamps(
2030 mergeMaps(snapshot1.fileTimestamps, snapshot2.fileTimestamps)
2031 );
2032 }
2033 if (snapshot1.hasFileHashes() || snapshot2.hasFileHashes()) {
2034 snapshot.setFileHashes(
2035 mergeMaps(snapshot1.fileHashes, snapshot2.fileHashes)
2036 );
2037 }
2038 if (snapshot1.hasFileTshs() || snapshot2.hasFileTshs()) {
2039 snapshot.setFileTshs(mergeMaps(snapshot1.fileTshs, snapshot2.fileTshs));
2040 }
2041 if (snapshot1.hasContextTimestamps() || snapshot2.hasContextTimestamps()) {
2042 snapshot.setContextTimestamps(
2043 mergeMaps(snapshot1.contextTimestamps, snapshot2.contextTimestamps)
2044 );
2045 }
2046 if (snapshot1.hasContextHashes() || snapshot2.hasContextHashes()) {
2047 snapshot.setContextHashes(
2048 mergeMaps(snapshot1.contextHashes, snapshot2.contextHashes)
2049 );
2050 }
2051 if (snapshot1.hasContextTshs() || snapshot2.hasContextTshs()) {
2052 snapshot.setContextTshs(
2053 mergeMaps(snapshot1.contextTshs, snapshot2.contextTshs)
2054 );
2055 }
2056 if (snapshot1.hasMissingExistence() || snapshot2.hasMissingExistence()) {
2057 snapshot.setMissingExistence(
2058 mergeMaps(snapshot1.missingExistence, snapshot2.missingExistence)
2059 );
2060 }
2061 if (snapshot1.hasManagedItemInfo() || snapshot2.hasManagedItemInfo()) {
2062 snapshot.setManagedItemInfo(
2063 mergeMaps(snapshot1.managedItemInfo, snapshot2.managedItemInfo)
2064 );
2065 }
2066 if (snapshot1.hasManagedFiles() || snapshot2.hasManagedFiles()) {
2067 snapshot.setManagedFiles(
2068 mergeSets(snapshot1.managedFiles, snapshot2.managedFiles)
2069 );
2070 }
2071 if (snapshot1.hasManagedContexts() || snapshot2.hasManagedContexts()) {
2072 snapshot.setManagedContexts(
2073 mergeSets(snapshot1.managedContexts, snapshot2.managedContexts)
2074 );
2075 }
2076 if (snapshot1.hasManagedMissing() || snapshot2.hasManagedMissing()) {
2077 snapshot.setManagedMissing(
2078 mergeSets(snapshot1.managedMissing, snapshot2.managedMissing)
2079 );
2080 }
2081 if (snapshot1.hasChildren() || snapshot2.hasChildren()) {
2082 snapshot.setChildren(mergeSets(snapshot1.children, snapshot2.children));
2083 }
2084 if (
2085 this._snapshotCache.get(snapshot1) === true &&
2086 this._snapshotCache.get(snapshot2) === true
2087 ) {
2088 this._snapshotCache.set(snapshot, true);
2089 }
2090 return snapshot;
2091 }
2092
2093 /**
2094 * @param {Snapshot} snapshot the snapshot made
2095 * @param {function(WebpackError=, boolean=): void} callback callback function
2096 * @returns {void}
2097 */
2098 checkSnapshotValid(snapshot, callback) {
2099 const cachedResult = this._snapshotCache.get(snapshot);
2100 if (cachedResult !== undefined) {
2101 this._statTestedSnapshotsCached++;
2102 if (typeof cachedResult === "boolean") {
2103 callback(null, cachedResult);
2104 } else {
2105 cachedResult.push(callback);
2106 }
2107 return;
2108 }
2109 this._statTestedSnapshotsNotCached++;
2110 this._checkSnapshotValidNoCache(snapshot, callback);
2111 }
2112
2113 /**
2114 * @param {Snapshot} snapshot the snapshot made
2115 * @param {function(WebpackError=, boolean=): void} callback callback function
2116 * @returns {void}
2117 */
2118 _checkSnapshotValidNoCache(snapshot, callback) {
2119 /** @type {number | undefined} */
2120 let startTime = undefined;
2121 if (snapshot.hasStartTime()) {
2122 startTime = snapshot.startTime;
2123 }
2124 let jobs = 1;
2125 const jobDone = () => {
2126 if (--jobs === 0) {
2127 this._snapshotCache.set(snapshot, true);
2128 callback(null, true);
2129 }
2130 };
2131 const invalid = () => {
2132 if (jobs > 0) {
2133 // large negative number instead of NaN or something else to keep jobs to stay a SMI (v8)
2134 jobs = -100000000;
2135 this._snapshotCache.set(snapshot, false);
2136 callback(null, false);
2137 }
2138 };
2139 const invalidWithError = (path, err) => {
2140 if (this._remainingLogs > 0) {
2141 this._log(path, `error occurred: %s`, err);
2142 }
2143 invalid();
2144 };
2145 /**
2146 * @param {string} path file path
2147 * @param {string} current current hash
2148 * @param {string} snap snapshot hash
2149 * @returns {boolean} true, if ok
2150 */
2151 const checkHash = (path, current, snap) => {
2152 if (current !== snap) {
2153 // If hash differ it's invalid
2154 if (this._remainingLogs > 0) {
2155 this._log(path, `hashes differ (%s != %s)`, current, snap);
2156 }
2157 return false;
2158 }
2159 return true;
2160 };
2161 /**
2162 * @param {string} path file path
2163 * @param {boolean} current current entry
2164 * @param {boolean} snap entry from snapshot
2165 * @returns {boolean} true, if ok
2166 */
2167 const checkExistence = (path, current, snap) => {
2168 if (!current !== !snap) {
2169 // If existence of item differs
2170 // it's invalid
2171 if (this._remainingLogs > 0) {
2172 this._log(
2173 path,
2174 current ? "it didn't exist before" : "it does no longer exist"
2175 );
2176 }
2177 return false;
2178 }
2179 return true;
2180 };
2181 /**
2182 * @param {string} path file path
2183 * @param {FileSystemInfoEntry} current current entry
2184 * @param {FileSystemInfoEntry} snap entry from snapshot
2185 * @param {boolean} log log reason
2186 * @returns {boolean} true, if ok
2187 */
2188 const checkFile = (path, current, snap, log = true) => {
2189 if (current === snap) return true;
2190 if (!current !== !snap) {
2191 // If existence of item differs
2192 // it's invalid
2193 if (log && this._remainingLogs > 0) {
2194 this._log(
2195 path,
2196 current ? "it didn't exist before" : "it does no longer exist"
2197 );
2198 }
2199 return false;
2200 }
2201 if (current) {
2202 // For existing items only
2203 if (typeof startTime === "number" && current.safeTime > startTime) {
2204 // If a change happened after starting reading the item
2205 // this may no longer be valid
2206 if (log && this._remainingLogs > 0) {
2207 this._log(
2208 path,
2209 `it may have changed (%d) after the start time of the snapshot (%d)`,
2210 current.safeTime,
2211 startTime
2212 );
2213 }
2214 return false;
2215 }
2216 if (
2217 snap.timestamp !== undefined &&
2218 current.timestamp !== snap.timestamp
2219 ) {
2220 // If we have a timestamp (it was a file or symlink) and it differs from current timestamp
2221 // it's invalid
2222 if (log && this._remainingLogs > 0) {
2223 this._log(
2224 path,
2225 `timestamps differ (%d != %d)`,
2226 current.timestamp,
2227 snap.timestamp
2228 );
2229 }
2230 return false;
2231 }
2232 if (
2233 snap.timestampHash !== undefined &&
2234 current.timestampHash !== snap.timestampHash
2235 ) {
2236 // If we have a timestampHash (it was a directory) and it differs from current timestampHash
2237 // it's invalid
2238 if (log && this._remainingLogs > 0) {
2239 this._log(
2240 path,
2241 `timestamps hashes differ (%s != %s)`,
2242 current.timestampHash,
2243 snap.timestampHash
2244 );
2245 }
2246 return false;
2247 }
2248 }
2249 return true;
2250 };
2251 if (snapshot.hasChildren()) {
2252 const childCallback = (err, result) => {
2253 if (err || !result) return invalid();
2254 else jobDone();
2255 };
2256 for (const child of snapshot.children) {
2257 const cache = this._snapshotCache.get(child);
2258 if (cache !== undefined) {
2259 this._statTestedChildrenCached++;
2260 /* istanbul ignore else */
2261 if (typeof cache === "boolean") {
2262 if (cache === false) {
2263 invalid();
2264 return;
2265 }
2266 } else {
2267 jobs++;
2268 cache.push(childCallback);
2269 }
2270 } else {
2271 this._statTestedChildrenNotCached++;
2272 jobs++;
2273 this._checkSnapshotValidNoCache(child, childCallback);
2274 }
2275 }
2276 }
2277 if (snapshot.hasFileTimestamps()) {
2278 const { fileTimestamps } = snapshot;
2279 this._statTestedEntries += fileTimestamps.size;
2280 for (const [path, ts] of fileTimestamps) {
2281 const cache = this._fileTimestamps.get(path);
2282 if (cache !== undefined) {
2283 if (cache !== "ignore" && !checkFile(path, cache, ts)) {
2284 invalid();
2285 return;
2286 }
2287 } else {
2288 jobs++;
2289 this.fileTimestampQueue.add(path, (err, entry) => {
2290 if (err) return invalidWithError(path, err);
2291 if (!checkFile(path, entry, ts)) {
2292 invalid();
2293 } else {
2294 jobDone();
2295 }
2296 });
2297 }
2298 }
2299 }
2300 const processFileHashSnapshot = (path, hash) => {
2301 const cache = this._fileHashes.get(path);
2302 if (cache !== undefined) {
2303 if (cache !== "ignore" && !checkHash(path, cache, hash)) {
2304 invalid();
2305 return;
2306 }
2307 } else {
2308 jobs++;
2309 this.fileHashQueue.add(path, (err, entry) => {
2310 if (err) return invalidWithError(path, err);
2311 if (!checkHash(path, entry, hash)) {
2312 invalid();
2313 } else {
2314 jobDone();
2315 }
2316 });
2317 }
2318 };
2319 if (snapshot.hasFileHashes()) {
2320 const { fileHashes } = snapshot;
2321 this._statTestedEntries += fileHashes.size;
2322 for (const [path, hash] of fileHashes) {
2323 processFileHashSnapshot(path, hash);
2324 }
2325 }
2326 if (snapshot.hasFileTshs()) {
2327 const { fileTshs } = snapshot;
2328 this._statTestedEntries += fileTshs.size;
2329 for (const [path, tsh] of fileTshs) {
2330 if (typeof tsh === "string") {
2331 processFileHashSnapshot(path, tsh);
2332 } else {
2333 const cache = this._fileTimestamps.get(path);
2334 if (cache !== undefined) {
2335 if (cache === "ignore" || !checkFile(path, cache, tsh, false)) {
2336 processFileHashSnapshot(path, tsh.hash);
2337 }
2338 } else {
2339 jobs++;
2340 this.fileTimestampQueue.add(path, (err, entry) => {
2341 if (err) return invalidWithError(path, err);
2342 if (!checkFile(path, entry, tsh, false)) {
2343 processFileHashSnapshot(path, tsh.hash);
2344 }
2345 jobDone();
2346 });
2347 }
2348 }
2349 }
2350 }
2351 if (snapshot.hasContextTimestamps()) {
2352 const { contextTimestamps } = snapshot;
2353 this._statTestedEntries += contextTimestamps.size;
2354 for (const [path, ts] of contextTimestamps) {
2355 const cache = this._contextTimestamps.get(path);
2356 if (cache !== undefined) {
2357 if (cache !== "ignore" && !checkFile(path, cache, ts)) {
2358 invalid();
2359 return;
2360 }
2361 } else {
2362 jobs++;
2363 this.contextTimestampQueue.add(path, (err, entry) => {
2364 if (err) return invalidWithError(path, err);
2365 if (!checkFile(path, entry, ts)) {
2366 invalid();
2367 } else {
2368 jobDone();
2369 }
2370 });
2371 }
2372 }
2373 }
2374 const processContextHashSnapshot = (path, hash) => {
2375 const cache = this._contextHashes.get(path);
2376 if (cache !== undefined) {
2377 if (cache !== "ignore" && !checkHash(path, cache, hash)) {
2378 invalid();
2379 return;
2380 }
2381 } else {
2382 jobs++;
2383 this.contextHashQueue.add(path, (err, entry) => {
2384 if (err) return invalidWithError(path, err);
2385 if (!checkHash(path, entry, hash)) {
2386 invalid();
2387 } else {
2388 jobDone();
2389 }
2390 });
2391 }
2392 };
2393 if (snapshot.hasContextHashes()) {
2394 const { contextHashes } = snapshot;
2395 this._statTestedEntries += contextHashes.size;
2396 for (const [path, hash] of contextHashes) {
2397 processContextHashSnapshot(path, hash);
2398 }
2399 }
2400 if (snapshot.hasContextTshs()) {
2401 const { contextTshs } = snapshot;
2402 this._statTestedEntries += contextTshs.size;
2403 for (const [path, tsh] of contextTshs) {
2404 if (typeof tsh === "string") {
2405 processContextHashSnapshot(path, tsh);
2406 } else {
2407 const cache = this._contextTimestamps.get(path);
2408 if (cache !== undefined) {
2409 if (cache === "ignore" || !checkFile(path, cache, tsh, false)) {
2410 processContextHashSnapshot(path, tsh.hash);
2411 }
2412 } else {
2413 jobs++;
2414 this.contextTimestampQueue.add(path, (err, entry) => {
2415 if (err) return invalidWithError(path, err);
2416 if (!checkFile(path, entry, tsh, false)) {
2417 processContextHashSnapshot(path, tsh.hash);
2418 }
2419 jobDone();
2420 });
2421 }
2422 }
2423 }
2424 }
2425 if (snapshot.hasMissingExistence()) {
2426 const { missingExistence } = snapshot;
2427 this._statTestedEntries += missingExistence.size;
2428 for (const [path, existence] of missingExistence) {
2429 const cache = this._fileTimestamps.get(path);
2430 if (cache !== undefined) {
2431 if (
2432 cache !== "ignore" &&
2433 !checkExistence(path, toExistence(cache), existence)
2434 ) {
2435 invalid();
2436 return;
2437 }
2438 } else {
2439 jobs++;
2440 this.fileTimestampQueue.add(path, (err, entry) => {
2441 if (err) return invalidWithError(path, err);
2442 if (!checkExistence(path, toExistence(entry), existence)) {
2443 invalid();
2444 } else {
2445 jobDone();
2446 }
2447 });
2448 }
2449 }
2450 }
2451 if (snapshot.hasManagedItemInfo()) {
2452 const { managedItemInfo } = snapshot;
2453 this._statTestedEntries += managedItemInfo.size;
2454 for (const [path, info] of managedItemInfo) {
2455 const cache = this._managedItems.get(path);
2456 if (cache !== undefined) {
2457 if (!checkHash(path, cache, info)) {
2458 invalid();
2459 return;
2460 }
2461 } else {
2462 jobs++;
2463 this.managedItemQueue.add(path, (err, entry) => {
2464 if (err) return invalidWithError(path, err);
2465 if (!checkHash(path, entry, info)) {
2466 invalid();
2467 } else {
2468 jobDone();
2469 }
2470 });
2471 }
2472 }
2473 }
2474 jobDone();
2475
2476 // if there was an async action
2477 // try to join multiple concurrent request for this snapshot
2478 if (jobs > 0) {
2479 const callbacks = [callback];
2480 callback = (err, result) => {
2481 for (const callback of callbacks) callback(err, result);
2482 };
2483 this._snapshotCache.set(snapshot, callbacks);
2484 }
2485 }
2486
2487 _readFileTimestamp(path, callback) {
2488 this.fs.stat(path, (err, stat) => {
2489 if (err) {
2490 if (err.code === "ENOENT") {
2491 this._fileTimestamps.set(path, null);
2492 this._cachedDeprecatedFileTimestamps = undefined;
2493 return callback(null, null);
2494 }
2495 return callback(err);
2496 }
2497
2498 let ts;
2499 if (stat.isDirectory()) {
2500 ts = {
2501 safeTime: 0,
2502 timestamp: undefined
2503 };
2504 } else {
2505 const mtime = +stat.mtime;
2506
2507 if (mtime) applyMtime(mtime);
2508
2509 ts = {
2510 safeTime: mtime ? mtime + FS_ACCURACY : Infinity,
2511 timestamp: mtime
2512 };
2513 }
2514
2515 this._fileTimestamps.set(path, ts);
2516 this._cachedDeprecatedFileTimestamps = undefined;
2517
2518 callback(null, ts);
2519 });
2520 }
2521
2522 _readFileHash(path, callback) {
2523 this.fs.readFile(path, (err, content) => {
2524 if (err) {
2525 if (err.code === "EISDIR") {
2526 this._fileHashes.set(path, "directory");
2527 return callback(null, "directory");
2528 }
2529 if (err.code === "ENOENT") {
2530 this._fileHashes.set(path, null);
2531 return callback(null, null);
2532 }
2533 if (err.code === "ERR_FS_FILE_TOO_LARGE") {
2534 this.logger.warn(`Ignoring ${path} for hashing as it's very large`);
2535 this._fileHashes.set(path, "too large");
2536 return callback(null, "too large");
2537 }
2538 return callback(err);
2539 }
2540
2541 const hash = createHash("md4");
2542
2543 hash.update(content);
2544
2545 const digest = /** @type {string} */ (hash.digest("hex"));
2546
2547 this._fileHashes.set(path, digest);
2548
2549 callback(null, digest);
2550 });
2551 }
2552
2553 _getFileTimestampAndHash(path, callback) {
2554 const continueWithHash = hash => {
2555 const cache = this._fileTimestamps.get(path);
2556 if (cache !== undefined) {
2557 if (cache !== "ignore") {
2558 const result = {
2559 ...cache,
2560 hash
2561 };
2562 this._fileTshs.set(path, result);
2563 return callback(null, result);
2564 } else {
2565 this._fileTshs.set(path, hash);
2566 return callback(null, hash);
2567 }
2568 } else {
2569 this.fileTimestampQueue.add(path, (err, entry) => {
2570 if (err) {
2571 return callback(err);
2572 }
2573 const result = {
2574 ...entry,
2575 hash
2576 };
2577 this._fileTshs.set(path, result);
2578 return callback(null, result);
2579 });
2580 }
2581 };
2582
2583 const cache = this._fileHashes.get(path);
2584 if (cache !== undefined) {
2585 continueWithHash(cache);
2586 } else {
2587 this.fileHashQueue.add(path, (err, entry) => {
2588 if (err) {
2589 return callback(err);
2590 }
2591 continueWithHash(entry);
2592 });
2593 }
2594 }
2595
2596 _readContextTimestamp(path, callback) {
2597 this.fs.readdir(path, (err, _files) => {
2598 if (err) {
2599 if (err.code === "ENOENT") {
2600 this._contextTimestamps.set(path, null);
2601 this._cachedDeprecatedContextTimestamps = undefined;
2602 return callback(null, null);
2603 }
2604 return callback(err);
2605 }
2606 const files = /** @type {string[]} */ (_files)
2607 .map(file => file.normalize("NFC"))
2608 .filter(file => !/^\./.test(file))
2609 .sort();
2610 asyncLib.map(
2611 files,
2612 (file, callback) => {
2613 const child = join(this.fs, path, file);
2614 this.fs.stat(child, (err, stat) => {
2615 if (err) return callback(err);
2616
2617 for (const immutablePath of this.immutablePathsWithSlash) {
2618 if (path.startsWith(immutablePath)) {
2619 // ignore any immutable path for timestamping
2620 return callback(null, null);
2621 }
2622 }
2623 for (const managedPath of this.managedPathsWithSlash) {
2624 if (path.startsWith(managedPath)) {
2625 const managedItem = getManagedItem(managedPath, child);
2626 if (managedItem) {
2627 // construct timestampHash from managed info
2628 return this.managedItemQueue.add(managedItem, (err, info) => {
2629 if (err) return callback(err);
2630 return callback(null, {
2631 safeTime: 0,
2632 timestampHash: info
2633 });
2634 });
2635 }
2636 }
2637 }
2638
2639 if (stat.isFile()) {
2640 return this.getFileTimestamp(child, callback);
2641 }
2642 if (stat.isDirectory()) {
2643 this.contextTimestampQueue.increaseParallelism();
2644 this.getContextTimestamp(child, (err, tsEntry) => {
2645 this.contextTimestampQueue.decreaseParallelism();
2646 callback(err, tsEntry);
2647 });
2648 return;
2649 }
2650 callback(null, null);
2651 });
2652 },
2653 (err, tsEntries) => {
2654 if (err) return callback(err);
2655 const hash = createHash("md4");
2656
2657 for (const file of files) hash.update(file);
2658 let safeTime = 0;
2659 for (const entry of tsEntries) {
2660 if (!entry) {
2661 hash.update("n");
2662 continue;
2663 }
2664 if (entry.timestamp) {
2665 hash.update("f");
2666 hash.update(`${entry.timestamp}`);
2667 } else if (entry.timestampHash) {
2668 hash.update("d");
2669 hash.update(`${entry.timestampHash}`);
2670 }
2671 if (entry.safeTime) {
2672 safeTime = Math.max(safeTime, entry.safeTime);
2673 }
2674 }
2675
2676 const digest = /** @type {string} */ (hash.digest("hex"));
2677
2678 const result = {
2679 safeTime,
2680 timestampHash: digest
2681 };
2682
2683 this._contextTimestamps.set(path, result);
2684 this._cachedDeprecatedContextTimestamps = undefined;
2685
2686 callback(null, result);
2687 }
2688 );
2689 });
2690 }
2691
2692 _readContextHash(path, callback) {
2693 this.fs.readdir(path, (err, _files) => {
2694 if (err) {
2695 if (err.code === "ENOENT") {
2696 this._contextHashes.set(path, null);
2697 return callback(null, null);
2698 }
2699 return callback(err);
2700 }
2701 const files = /** @type {string[]} */ (_files)
2702 .map(file => file.normalize("NFC"))
2703 .filter(file => !/^\./.test(file))
2704 .sort();
2705 asyncLib.map(
2706 files,
2707 (file, callback) => {
2708 const child = join(this.fs, path, file);
2709 this.fs.stat(child, (err, stat) => {
2710 if (err) return callback(err);
2711
2712 for (const immutablePath of this.immutablePathsWithSlash) {
2713 if (path.startsWith(immutablePath)) {
2714 // ignore any immutable path for hashing
2715 return callback(null, "");
2716 }
2717 }
2718 for (const managedPath of this.managedPathsWithSlash) {
2719 if (path.startsWith(managedPath)) {
2720 const managedItem = getManagedItem(managedPath, child);
2721 if (managedItem) {
2722 // construct hash from managed info
2723 return this.managedItemQueue.add(managedItem, (err, info) => {
2724 if (err) return callback(err);
2725 callback(null, info || "");
2726 });
2727 }
2728 }
2729 }
2730
2731 if (stat.isFile()) {
2732 return this.getFileHash(child, (err, hash) => {
2733 callback(err, hash || "");
2734 });
2735 }
2736 if (stat.isDirectory()) {
2737 this.contextHashQueue.increaseParallelism();
2738 this.getContextHash(child, (err, hash) => {
2739 this.contextHashQueue.decreaseParallelism();
2740 callback(err, hash || "");
2741 });
2742 return;
2743 }
2744 callback(null, "");
2745 });
2746 },
2747 (err, fileHashes) => {
2748 if (err) return callback(err);
2749 const hash = createHash("md4");
2750
2751 for (const file of files) hash.update(file);
2752 for (const h of fileHashes) hash.update(h);
2753
2754 const digest = /** @type {string} */ (hash.digest("hex"));
2755
2756 this._contextHashes.set(path, digest);
2757
2758 callback(null, digest);
2759 }
2760 );
2761 });
2762 }
2763
2764 _getContextTimestampAndHash(path, callback) {
2765 const continueWithHash = hash => {
2766 const cache = this._contextTimestamps.get(path);
2767 if (cache !== undefined) {
2768 if (cache !== "ignore") {
2769 const result = {
2770 ...cache,
2771 hash
2772 };
2773 this._contextTshs.set(path, result);
2774 return callback(null, result);
2775 } else {
2776 this._contextTshs.set(path, hash);
2777 return callback(null, hash);
2778 }
2779 } else {
2780 this.contextTimestampQueue.add(path, (err, entry) => {
2781 if (err) {
2782 return callback(err);
2783 }
2784 const result = {
2785 ...entry,
2786 hash
2787 };
2788 this._contextTshs.set(path, result);
2789 return callback(null, result);
2790 });
2791 }
2792 };
2793
2794 const cache = this._contextHashes.get(path);
2795 if (cache !== undefined) {
2796 continueWithHash(cache);
2797 } else {
2798 this.contextHashQueue.add(path, (err, entry) => {
2799 if (err) {
2800 return callback(err);
2801 }
2802 continueWithHash(entry);
2803 });
2804 }
2805 }
2806
2807 _getManagedItemDirectoryInfo(path, callback) {
2808 this.fs.readdir(path, (err, elements) => {
2809 if (err) {
2810 if (err.code === "ENOENT" || err.code === "ENOTDIR") {
2811 return callback(null, EMPTY_SET);
2812 }
2813 return callback(err);
2814 }
2815 const set = new Set(
2816 /** @type {string[]} */ (elements).map(element =>
2817 join(this.fs, path, element)
2818 )
2819 );
2820 callback(null, set);
2821 });
2822 }
2823
2824 _getManagedItemInfo(path, callback) {
2825 const dir = dirname(this.fs, path);
2826 this.managedItemDirectoryQueue.add(dir, (err, elements) => {
2827 if (err) {
2828 return callback(err);
2829 }
2830 if (!elements.has(path)) {
2831 // file or directory doesn't exist
2832 this._managedItems.set(path, "missing");
2833 return callback(null, "missing");
2834 }
2835 // something exists
2836 // it may be a file or directory
2837 if (
2838 path.endsWith("node_modules") &&
2839 (path.endsWith("/node_modules") || path.endsWith("\\node_modules"))
2840 ) {
2841 // we are only interested in existence of this special directory
2842 this._managedItems.set(path, "exists");
2843 return callback(null, "exists");
2844 }
2845
2846 // we assume it's a directory, as files shouldn't occur in managed paths
2847 const packageJsonPath = join(this.fs, path, "package.json");
2848 this.fs.readFile(packageJsonPath, (err, content) => {
2849 if (err) {
2850 if (err.code === "ENOENT" || err.code === "ENOTDIR") {
2851 // no package.json or path is not a directory
2852 this.fs.readdir(path, (err, elements) => {
2853 if (
2854 !err &&
2855 elements.length === 1 &&
2856 elements[0] === "node_modules"
2857 ) {
2858 // This is only a grouping folder e. g. used by yarn
2859 // we are only interested in existence of this special directory
2860 this._managedItems.set(path, "nested");
2861 return callback(null, "nested");
2862 }
2863 const problem = `Managed item ${path} isn't a directory or doesn't contain a package.json`;
2864 this.logger.warn(problem);
2865 return callback(new Error(problem));
2866 });
2867 return;
2868 }
2869 return callback(err);
2870 }
2871 let data;
2872 try {
2873 data = JSON.parse(content.toString("utf-8"));
2874 } catch (e) {
2875 return callback(e);
2876 }
2877 const info = `${data.name || ""}@${data.version || ""}`;
2878 this._managedItems.set(path, info);
2879 callback(null, info);
2880 });
2881 });
2882 }
2883
2884 getDeprecatedFileTimestamps() {
2885 if (this._cachedDeprecatedFileTimestamps !== undefined)
2886 return this._cachedDeprecatedFileTimestamps;
2887 const map = new Map();
2888 for (const [path, info] of this._fileTimestamps) {
2889 if (info) map.set(path, typeof info === "object" ? info.safeTime : null);
2890 }
2891 return (this._cachedDeprecatedFileTimestamps = map);
2892 }
2893
2894 getDeprecatedContextTimestamps() {
2895 if (this._cachedDeprecatedContextTimestamps !== undefined)
2896 return this._cachedDeprecatedContextTimestamps;
2897 const map = new Map();
2898 for (const [path, info] of this._contextTimestamps) {
2899 if (info) map.set(path, typeof info === "object" ? info.safeTime : null);
2900 }
2901 return (this._cachedDeprecatedContextTimestamps = map);
2902 }
2903}
2904
2905module.exports = FileSystemInfo;
2906module.exports.Snapshot = Snapshot;