UNPKG

81.5 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 processAsyncTree(
1111 Array.from(deps, dep => ({
1112 type: RBDT_RESOLVE_CJS,
1113 context,
1114 path: dep,
1115 expected: undefined
1116 })),
1117 20,
1118 ({ type, context, path, expected }, push, callback) => {
1119 const resolveDirectory = path => {
1120 const key = `d\n${context}\n${path}`;
1121 if (resolveResults.has(key)) {
1122 return callback();
1123 }
1124 resolveResults.set(key, undefined);
1125 resolveContext(context, path, resolverContext, (err, result) => {
1126 if (err) {
1127 invalidResolveResults.add(key);
1128 if (
1129 err.code === "ENOENT" ||
1130 err.code === "UNDECLARED_DEPENDENCY"
1131 ) {
1132 return callback();
1133 }
1134 err.message += `\nwhile resolving '${path}' in ${context} to a directory`;
1135 return callback(err);
1136 }
1137 resolveResults.set(key, result);
1138 push({
1139 type: RBDT_DIRECTORY,
1140 context: undefined,
1141 path: result,
1142 expected: undefined
1143 });
1144 callback();
1145 });
1146 };
1147 const resolveFile = (path, symbol, resolve) => {
1148 const key = `${symbol}\n${context}\n${path}`;
1149 if (resolveResults.has(key)) {
1150 return callback();
1151 }
1152 resolveResults.set(key, undefined);
1153 resolve(context, path, resolverContext, (err, result) => {
1154 if (expected) {
1155 if (result === expected) {
1156 resolveResults.set(key, result);
1157 } else {
1158 invalidResolveResults.add(key);
1159 this.logger.debug(
1160 `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.`
1161 );
1162 }
1163 } else {
1164 if (err) {
1165 invalidResolveResults.add(key);
1166 if (
1167 err.code === "ENOENT" ||
1168 err.code === "UNDECLARED_DEPENDENCY"
1169 ) {
1170 return callback();
1171 }
1172 err.message += `\nwhile resolving '${path}' in ${context} as file`;
1173 return callback(err);
1174 }
1175 resolveResults.set(key, result);
1176 push({
1177 type: RBDT_FILE,
1178 context: undefined,
1179 path: result,
1180 expected: undefined
1181 });
1182 }
1183 callback();
1184 });
1185 };
1186 switch (type) {
1187 case RBDT_RESOLVE_CJS: {
1188 const isDirectory = /[\\/]$/.test(path);
1189 if (isDirectory) {
1190 resolveDirectory(path.slice(0, path.length - 1));
1191 } else {
1192 resolveFile(path, "f", resolveCjs);
1193 }
1194 break;
1195 }
1196 case RBDT_RESOLVE_ESM: {
1197 const isDirectory = /[\\/]$/.test(path);
1198 if (isDirectory) {
1199 resolveDirectory(path.slice(0, path.length - 1));
1200 } else {
1201 resolveFile(path);
1202 }
1203 break;
1204 }
1205 case RBDT_RESOLVE_DIRECTORY: {
1206 resolveDirectory(path);
1207 break;
1208 }
1209 case RBDT_RESOLVE_CJS_FILE: {
1210 resolveFile(path, "f", resolveCjs);
1211 break;
1212 }
1213 case RBDT_RESOLVE_ESM_FILE: {
1214 resolveFile(path, "e", resolveEsm);
1215 break;
1216 }
1217 case RBDT_FILE: {
1218 if (files.has(path)) {
1219 callback();
1220 break;
1221 }
1222 files.add(path);
1223 this.fs.realpath(path, (err, _realPath) => {
1224 if (err) return callback(err);
1225 const realPath = /** @type {string} */ (_realPath);
1226 if (realPath !== path) {
1227 fileSymlinks.add(path);
1228 resolveFiles.add(path);
1229 if (files.has(realPath)) return callback();
1230 files.add(realPath);
1231 }
1232 push({
1233 type: RBDT_FILE_DEPENDENCIES,
1234 context: undefined,
1235 path: realPath,
1236 expected: undefined
1237 });
1238 callback();
1239 });
1240 break;
1241 }
1242 case RBDT_DIRECTORY: {
1243 if (directories.has(path)) {
1244 callback();
1245 break;
1246 }
1247 directories.add(path);
1248 this.fs.realpath(path, (err, _realPath) => {
1249 if (err) return callback(err);
1250 const realPath = /** @type {string} */ (_realPath);
1251 if (realPath !== path) {
1252 directorySymlinks.add(path);
1253 resolveFiles.add(path);
1254 if (directories.has(realPath)) return callback();
1255 directories.add(realPath);
1256 }
1257 push({
1258 type: RBDT_DIRECTORY_DEPENDENCIES,
1259 context: undefined,
1260 path: realPath,
1261 expected: undefined
1262 });
1263 callback();
1264 });
1265 break;
1266 }
1267 case RBDT_FILE_DEPENDENCIES: {
1268 // Check for known files without dependencies
1269 if (/\.json5?$|\.yarn-integrity$|yarn\.lock$|\.ya?ml/.test(path)) {
1270 process.nextTick(callback);
1271 break;
1272 }
1273 // Check commonjs cache for the module
1274 /** @type {NodeModule} */
1275 const module = require.cache[path];
1276 if (module && Array.isArray(module.children)) {
1277 children: for (const child of module.children) {
1278 let childPath = child.filename;
1279 if (childPath) {
1280 push({
1281 type: RBDT_FILE,
1282 context: undefined,
1283 path: childPath,
1284 expected: undefined
1285 });
1286 const context = dirname(this.fs, path);
1287 for (const modulePath of module.paths) {
1288 if (childPath.startsWith(modulePath)) {
1289 let request = childPath.slice(modulePath.length + 1);
1290 if (request.endsWith(".js"))
1291 request = request.slice(0, -3);
1292 push({
1293 type: RBDT_RESOLVE_CJS_FILE,
1294 context,
1295 path: request,
1296 expected: child.filename
1297 });
1298 continue children;
1299 }
1300 }
1301 let request = relative(this.fs, context, childPath);
1302 if (request.endsWith(".js")) request = request.slice(0, -3);
1303 request = request.replace(/\\/g, "/");
1304 if (!request.startsWith("../")) request = `./${request}`;
1305 push({
1306 type: RBDT_RESOLVE_CJS_FILE,
1307 context,
1308 path: request,
1309 expected: child.filename
1310 });
1311 }
1312 }
1313 } else if (supportsEsm && /\.m?js$/.test(path)) {
1314 if (!this._warnAboutExperimentalEsmTracking) {
1315 this.logger.info(
1316 "Node.js doesn't offer a (nice) way to introspect the ESM dependency graph yet.\n" +
1317 "Until a full solution is available webpack uses an experimental ESM tracking based on parsing.\n" +
1318 "As best effort webpack parses the ESM files to guess dependencies. But this can lead to expensive and incorrect tracking."
1319 );
1320 this._warnAboutExperimentalEsmTracking = true;
1321 }
1322 const lexer = require("es-module-lexer");
1323 lexer.init.then(() => {
1324 this.fs.readFile(path, (err, content) => {
1325 if (err) return callback(err);
1326 try {
1327 const context = dirname(this.fs, path);
1328 const source = content.toString();
1329 const [imports] = lexer.parse(source);
1330 for (const imp of imports) {
1331 try {
1332 let dependency;
1333 if (imp.d === -1) {
1334 // import ... from "..."
1335 dependency = JSON.parse(
1336 source.substring(imp.s - 1, imp.e + 1)
1337 );
1338 } else if (imp.d > -1) {
1339 // import()
1340 let expr = source.substring(imp.s, imp.e).trim();
1341 if (expr[0] === "'")
1342 expr = `"${expr
1343 .slice(1, -1)
1344 .replace(/"/g, '\\"')}"`;
1345 dependency = JSON.parse(expr);
1346 } else {
1347 // e.g. import.meta
1348 continue;
1349 }
1350 push({
1351 type: RBDT_RESOLVE_ESM_FILE,
1352 context,
1353 path: dependency,
1354 expected: undefined
1355 });
1356 } catch (e) {
1357 this.logger.warn(
1358 `Parsing of ${path} for build dependencies failed at 'import(${source.substring(
1359 imp.s,
1360 imp.e
1361 )})'.\n` +
1362 "Build dependencies behind this expression are ignored and might cause incorrect cache invalidation."
1363 );
1364 this.logger.debug(e.stack);
1365 }
1366 }
1367 } catch (e) {
1368 this.logger.warn(
1369 `Parsing of ${path} for build dependencies failed and all dependencies of this file are ignored, which might cause incorrect cache invalidation..`
1370 );
1371 this.logger.debug(e.stack);
1372 }
1373 process.nextTick(callback);
1374 });
1375 }, callback);
1376 break;
1377 } else {
1378 this.logger.log(
1379 `Assuming ${path} has no dependencies as we were unable to assign it to any module system.`
1380 );
1381 }
1382 process.nextTick(callback);
1383 break;
1384 }
1385 case RBDT_DIRECTORY_DEPENDENCIES: {
1386 const match = /(^.+[\\/]node_modules[\\/](?:@[^\\/]+[\\/])?[^\\/]+)/.exec(
1387 path
1388 );
1389 const packagePath = match ? match[1] : path;
1390 const packageJson = join(this.fs, packagePath, "package.json");
1391 this.fs.readFile(packageJson, (err, content) => {
1392 if (err) {
1393 if (err.code === "ENOENT") {
1394 resolveMissing.add(packageJson);
1395 const parent = dirname(this.fs, packagePath);
1396 if (parent !== packagePath) {
1397 push({
1398 type: RBDT_DIRECTORY_DEPENDENCIES,
1399 context: undefined,
1400 path: parent,
1401 expected: undefined
1402 });
1403 }
1404 callback();
1405 return;
1406 }
1407 return callback(err);
1408 }
1409 resolveFiles.add(packageJson);
1410 let packageData;
1411 try {
1412 packageData = JSON.parse(content.toString("utf-8"));
1413 } catch (e) {
1414 return callback(e);
1415 }
1416 const depsObject = packageData.dependencies;
1417 if (typeof depsObject === "object" && depsObject) {
1418 for (const dep of Object.keys(depsObject)) {
1419 push({
1420 type: RBDT_RESOLVE_DIRECTORY,
1421 context: packagePath,
1422 path: dep,
1423 expected: undefined
1424 });
1425 }
1426 }
1427 callback();
1428 });
1429 break;
1430 }
1431 }
1432 },
1433 err => {
1434 if (err) return callback(err);
1435 for (const l of fileSymlinks) files.delete(l);
1436 for (const l of directorySymlinks) directories.delete(l);
1437 for (const k of invalidResolveResults) resolveResults.delete(k);
1438 callback(null, {
1439 files,
1440 directories,
1441 missing,
1442 resolveResults,
1443 resolveDependencies: {
1444 files: resolveFiles,
1445 directories: resolveDirectories,
1446 missing: resolveMissing
1447 }
1448 });
1449 }
1450 );
1451 }
1452
1453 /**
1454 * @param {Map<string, string>} resolveResults results from resolving
1455 * @param {function(Error=, boolean=): void} callback callback with true when resolveResults resolve the same way
1456 * @returns {void}
1457 */
1458 checkResolveResultsValid(resolveResults, callback) {
1459 const {
1460 resolveCjs,
1461 resolveEsm,
1462 resolveContext
1463 } = this._createBuildDependenciesResolvers();
1464 asyncLib.eachLimit(
1465 resolveResults,
1466 20,
1467 ([key, expectedResult], callback) => {
1468 const [type, context, path] = key.split("\n");
1469 switch (type) {
1470 case "d":
1471 resolveContext(context, path, {}, (err, result) => {
1472 if (err) return callback(err);
1473 if (result !== expectedResult) return callback(INVALID);
1474 callback();
1475 });
1476 break;
1477 case "f":
1478 resolveCjs(context, path, {}, (err, result) => {
1479 if (err) return callback(err);
1480 if (result !== expectedResult) return callback(INVALID);
1481 callback();
1482 });
1483 break;
1484 case "e":
1485 resolveEsm(context, path, {}, (err, result) => {
1486 if (err) return callback(err);
1487 if (result !== expectedResult) return callback(INVALID);
1488 callback();
1489 });
1490 break;
1491 default:
1492 callback(new Error("Unexpected type in resolve result key"));
1493 break;
1494 }
1495 },
1496 /**
1497 * @param {Error | typeof INVALID=} err error or invalid flag
1498 * @returns {void}
1499 */
1500 err => {
1501 if (err === INVALID) {
1502 return callback(null, false);
1503 }
1504 if (err) {
1505 return callback(err);
1506 }
1507 return callback(null, true);
1508 }
1509 );
1510 }
1511
1512 /**
1513 *
1514 * @param {number} startTime when processing the files has started
1515 * @param {Iterable<string>} files all files
1516 * @param {Iterable<string>} directories all directories
1517 * @param {Iterable<string>} missing all missing files or directories
1518 * @param {Object} options options object (for future extensions)
1519 * @param {boolean=} options.hash should use hash to snapshot
1520 * @param {boolean=} options.timestamp should use timestamp to snapshot
1521 * @param {function(WebpackError=, Snapshot=): void} callback callback function
1522 * @returns {void}
1523 */
1524 createSnapshot(startTime, files, directories, missing, options, callback) {
1525 /** @type {Map<string, FileSystemInfoEntry>} */
1526 const fileTimestamps = new Map();
1527 /** @type {Map<string, string>} */
1528 const fileHashes = new Map();
1529 /** @type {Map<string, TimestampAndHash | string>} */
1530 const fileTshs = new Map();
1531 /** @type {Map<string, FileSystemInfoEntry>} */
1532 const contextTimestamps = new Map();
1533 /** @type {Map<string, string>} */
1534 const contextHashes = new Map();
1535 /** @type {Map<string, TimestampAndHash | string>} */
1536 const contextTshs = new Map();
1537 /** @type {Map<string, boolean>} */
1538 const missingExistence = new Map();
1539 /** @type {Map<string, string>} */
1540 const managedItemInfo = new Map();
1541 /** @type {Set<string>} */
1542 const managedFiles = new Set();
1543 /** @type {Set<string>} */
1544 const managedContexts = new Set();
1545 /** @type {Set<string>} */
1546 const managedMissing = new Set();
1547 /** @type {Set<Snapshot>} */
1548 const children = new Set();
1549
1550 /** @type {Set<string>} */
1551 let unsharedFileTimestamps;
1552 /** @type {Set<string>} */
1553 let unsharedFileHashes;
1554 /** @type {Set<string>} */
1555 let unsharedFileTshs;
1556 /** @type {Set<string>} */
1557 let unsharedContextTimestamps;
1558 /** @type {Set<string>} */
1559 let unsharedContextHashes;
1560 /** @type {Set<string>} */
1561 let unsharedContextTshs;
1562 /** @type {Set<string>} */
1563 let unsharedMissingExistence;
1564 /** @type {Set<string>} */
1565 let unsharedManagedItemInfo;
1566
1567 /** @type {Set<string>} */
1568 const managedItems = new Set();
1569
1570 /** 1 = timestamp, 2 = hash, 3 = timestamp + hash */
1571 const mode = options && options.hash ? (options.timestamp ? 3 : 2) : 1;
1572
1573 let jobs = 1;
1574 const jobDone = () => {
1575 if (--jobs === 0) {
1576 const snapshot = new Snapshot();
1577 if (startTime) snapshot.setStartTime(startTime);
1578 if (fileTimestamps.size !== 0) {
1579 snapshot.setFileTimestamps(fileTimestamps);
1580 this._fileTimestampsOptimization.storeUnsharedSnapshot(
1581 snapshot,
1582 unsharedFileTimestamps
1583 );
1584 }
1585 if (fileHashes.size !== 0) {
1586 snapshot.setFileHashes(fileHashes);
1587 this._fileHashesOptimization.storeUnsharedSnapshot(
1588 snapshot,
1589 unsharedFileHashes
1590 );
1591 }
1592 if (fileTshs.size !== 0) {
1593 snapshot.setFileTshs(fileTshs);
1594 this._fileTshsOptimization.storeUnsharedSnapshot(
1595 snapshot,
1596 unsharedFileTshs
1597 );
1598 }
1599 if (contextTimestamps.size !== 0) {
1600 snapshot.setContextTimestamps(contextTimestamps);
1601 this._contextTimestampsOptimization.storeUnsharedSnapshot(
1602 snapshot,
1603 unsharedContextTimestamps
1604 );
1605 }
1606 if (contextHashes.size !== 0) {
1607 snapshot.setContextHashes(contextHashes);
1608 this._contextHashesOptimization.storeUnsharedSnapshot(
1609 snapshot,
1610 unsharedContextHashes
1611 );
1612 }
1613 if (contextTshs.size !== 0) {
1614 snapshot.setContextTshs(contextTshs);
1615 this._contextTshsOptimization.storeUnsharedSnapshot(
1616 snapshot,
1617 unsharedContextTshs
1618 );
1619 }
1620 if (missingExistence.size !== 0) {
1621 snapshot.setMissingExistence(missingExistence);
1622 this._missingExistenceOptimization.storeUnsharedSnapshot(
1623 snapshot,
1624 unsharedMissingExistence
1625 );
1626 }
1627 if (managedItemInfo.size !== 0) {
1628 snapshot.setManagedItemInfo(managedItemInfo);
1629 this._managedItemInfoOptimization.storeUnsharedSnapshot(
1630 snapshot,
1631 unsharedManagedItemInfo
1632 );
1633 }
1634 const unsharedManagedFiles = this._managedFilesOptimization.optimize(
1635 managedFiles,
1636 undefined,
1637 children
1638 );
1639 if (managedFiles.size !== 0) {
1640 snapshot.setManagedFiles(managedFiles);
1641 this._managedFilesOptimization.storeUnsharedSnapshot(
1642 snapshot,
1643 unsharedManagedFiles
1644 );
1645 }
1646 const unsharedManagedContexts = this._managedContextsOptimization.optimize(
1647 managedContexts,
1648 undefined,
1649 children
1650 );
1651 if (managedContexts.size !== 0) {
1652 snapshot.setManagedContexts(managedContexts);
1653 this._managedContextsOptimization.storeUnsharedSnapshot(
1654 snapshot,
1655 unsharedManagedContexts
1656 );
1657 }
1658 const unsharedManagedMissing = this._managedMissingOptimization.optimize(
1659 managedMissing,
1660 undefined,
1661 children
1662 );
1663 if (managedMissing.size !== 0) {
1664 snapshot.setManagedMissing(managedMissing);
1665 this._managedMissingOptimization.storeUnsharedSnapshot(
1666 snapshot,
1667 unsharedManagedMissing
1668 );
1669 }
1670 if (children.size !== 0) {
1671 snapshot.setChildren(children);
1672 }
1673 this._snapshotCache.set(snapshot, true);
1674 this._statCreatedSnapshots++;
1675
1676 callback(null, snapshot);
1677 }
1678 };
1679 const jobError = () => {
1680 if (jobs > 0) {
1681 // large negative number instead of NaN or something else to keep jobs to stay a SMI (v8)
1682 jobs = -100000000;
1683 callback(null, null);
1684 }
1685 };
1686 const checkManaged = (path, managedSet) => {
1687 for (const immutablePath of this.immutablePathsWithSlash) {
1688 if (path.startsWith(immutablePath)) {
1689 managedSet.add(path);
1690 return true;
1691 }
1692 }
1693 for (const managedPath of this.managedPathsWithSlash) {
1694 if (path.startsWith(managedPath)) {
1695 const managedItem = getManagedItem(managedPath, path);
1696 if (managedItem) {
1697 managedItems.add(managedItem);
1698 managedSet.add(path);
1699 return true;
1700 }
1701 }
1702 }
1703 return false;
1704 };
1705 const captureNonManaged = (items, managedSet) => {
1706 const capturedItems = new Set();
1707 for (const path of items) {
1708 if (!checkManaged(path, managedSet)) capturedItems.add(path);
1709 }
1710 return capturedItems;
1711 };
1712 if (files) {
1713 const capturedFiles = captureNonManaged(files, managedFiles);
1714 switch (mode) {
1715 case 3:
1716 unsharedFileTshs = this._fileTshsOptimization.optimize(
1717 capturedFiles,
1718 undefined,
1719 children
1720 );
1721 for (const path of capturedFiles) {
1722 const cache = this._fileTshs.get(path);
1723 if (cache !== undefined) {
1724 fileTshs.set(path, cache);
1725 } else {
1726 jobs++;
1727 this._getFileTimestampAndHash(path, (err, entry) => {
1728 if (err) {
1729 if (this.logger) {
1730 this.logger.debug(
1731 `Error snapshotting file timestamp hash combination of ${path}: ${err.stack}`
1732 );
1733 }
1734 jobError();
1735 } else {
1736 fileTshs.set(path, entry);
1737 jobDone();
1738 }
1739 });
1740 }
1741 }
1742 break;
1743 case 2:
1744 unsharedFileHashes = this._fileHashesOptimization.optimize(
1745 capturedFiles,
1746 undefined,
1747 children
1748 );
1749 for (const path of capturedFiles) {
1750 const cache = this._fileHashes.get(path);
1751 if (cache !== undefined) {
1752 fileHashes.set(path, cache);
1753 } else {
1754 jobs++;
1755 this.fileHashQueue.add(path, (err, entry) => {
1756 if (err) {
1757 if (this.logger) {
1758 this.logger.debug(
1759 `Error snapshotting file hash of ${path}: ${err.stack}`
1760 );
1761 }
1762 jobError();
1763 } else {
1764 fileHashes.set(path, entry);
1765 jobDone();
1766 }
1767 });
1768 }
1769 }
1770 break;
1771 case 1:
1772 unsharedFileTimestamps = this._fileTimestampsOptimization.optimize(
1773 capturedFiles,
1774 startTime,
1775 children
1776 );
1777 for (const path of capturedFiles) {
1778 const cache = this._fileTimestamps.get(path);
1779 if (cache !== undefined) {
1780 if (cache !== "ignore") {
1781 fileTimestamps.set(path, cache);
1782 }
1783 } else {
1784 jobs++;
1785 this.fileTimestampQueue.add(path, (err, entry) => {
1786 if (err) {
1787 if (this.logger) {
1788 this.logger.debug(
1789 `Error snapshotting file timestamp of ${path}: ${err.stack}`
1790 );
1791 }
1792 jobError();
1793 } else {
1794 fileTimestamps.set(path, entry);
1795 jobDone();
1796 }
1797 });
1798 }
1799 }
1800 break;
1801 }
1802 }
1803 if (directories) {
1804 const capturedDirectories = captureNonManaged(
1805 directories,
1806 managedContexts
1807 );
1808 switch (mode) {
1809 case 3:
1810 unsharedContextTshs = this._contextTshsOptimization.optimize(
1811 capturedDirectories,
1812 undefined,
1813 children
1814 );
1815 for (const path of capturedDirectories) {
1816 const cache = this._contextTshs.get(path);
1817 if (cache !== undefined) {
1818 contextTshs.set(path, cache);
1819 } else {
1820 jobs++;
1821 this._getContextTimestampAndHash(path, (err, entry) => {
1822 if (err) {
1823 if (this.logger) {
1824 this.logger.debug(
1825 `Error snapshotting context timestamp hash combination of ${path}: ${err.stack}`
1826 );
1827 }
1828 jobError();
1829 } else {
1830 contextTshs.set(path, entry);
1831 jobDone();
1832 }
1833 });
1834 }
1835 }
1836 break;
1837 case 2:
1838 unsharedContextHashes = this._contextHashesOptimization.optimize(
1839 capturedDirectories,
1840 undefined,
1841 children
1842 );
1843 for (const path of capturedDirectories) {
1844 const cache = this._contextHashes.get(path);
1845 if (cache !== undefined) {
1846 contextHashes.set(path, cache);
1847 } else {
1848 jobs++;
1849 this.contextHashQueue.add(path, (err, entry) => {
1850 if (err) {
1851 if (this.logger) {
1852 this.logger.debug(
1853 `Error snapshotting context hash of ${path}: ${err.stack}`
1854 );
1855 }
1856 jobError();
1857 } else {
1858 contextHashes.set(path, entry);
1859 jobDone();
1860 }
1861 });
1862 }
1863 }
1864 break;
1865 case 1:
1866 unsharedContextTimestamps = this._contextTimestampsOptimization.optimize(
1867 capturedDirectories,
1868 startTime,
1869 children
1870 );
1871 for (const path of capturedDirectories) {
1872 const cache = this._contextTimestamps.get(path);
1873 if (cache !== undefined) {
1874 if (cache !== "ignore") {
1875 contextTimestamps.set(path, cache);
1876 }
1877 } else {
1878 jobs++;
1879 this.contextTimestampQueue.add(path, (err, entry) => {
1880 if (err) {
1881 if (this.logger) {
1882 this.logger.debug(
1883 `Error snapshotting context timestamp of ${path}: ${err.stack}`
1884 );
1885 }
1886 jobError();
1887 } else {
1888 contextTimestamps.set(path, entry);
1889 jobDone();
1890 }
1891 });
1892 }
1893 }
1894 break;
1895 }
1896 }
1897 if (missing) {
1898 const capturedMissing = captureNonManaged(missing, managedMissing);
1899 unsharedMissingExistence = this._missingExistenceOptimization.optimize(
1900 capturedMissing,
1901 startTime,
1902 children
1903 );
1904 for (const path of capturedMissing) {
1905 const cache = this._fileTimestamps.get(path);
1906 if (cache !== undefined) {
1907 if (cache !== "ignore") {
1908 missingExistence.set(path, toExistence(cache));
1909 }
1910 } else {
1911 jobs++;
1912 this.fileTimestampQueue.add(path, (err, entry) => {
1913 if (err) {
1914 if (this.logger) {
1915 this.logger.debug(
1916 `Error snapshotting missing timestamp of ${path}: ${err.stack}`
1917 );
1918 }
1919 jobError();
1920 } else {
1921 missingExistence.set(path, toExistence(entry));
1922 jobDone();
1923 }
1924 });
1925 }
1926 }
1927 }
1928 unsharedManagedItemInfo = this._managedItemInfoOptimization.optimize(
1929 managedItems,
1930 undefined,
1931 children
1932 );
1933 for (const path of managedItems) {
1934 const cache = this._managedItems.get(path);
1935 if (cache !== undefined) {
1936 managedItemInfo.set(path, cache);
1937 } else {
1938 jobs++;
1939 this.managedItemQueue.add(path, (err, entry) => {
1940 if (err) {
1941 if (this.logger) {
1942 this.logger.debug(
1943 `Error snapshotting managed item ${path}: ${err.stack}`
1944 );
1945 }
1946 jobError();
1947 } else {
1948 managedItemInfo.set(path, entry);
1949 jobDone();
1950 }
1951 });
1952 }
1953 }
1954 jobDone();
1955 }
1956
1957 /**
1958 * @param {Snapshot} snapshot1 a snapshot
1959 * @param {Snapshot} snapshot2 a snapshot
1960 * @returns {Snapshot} merged snapshot
1961 */
1962 mergeSnapshots(snapshot1, snapshot2) {
1963 const snapshot = new Snapshot();
1964 if (snapshot1.hasStartTime() && snapshot2.hasStartTime())
1965 snapshot.setStartTime(Math.min(snapshot1.startTime, snapshot2.startTime));
1966 else if (snapshot2.hasStartTime()) snapshot.startTime = snapshot2.startTime;
1967 else if (snapshot1.hasStartTime()) snapshot.startTime = snapshot1.startTime;
1968 if (snapshot1.hasFileTimestamps() || snapshot2.hasFileTimestamps()) {
1969 snapshot.setFileTimestamps(
1970 mergeMaps(snapshot1.fileTimestamps, snapshot2.fileTimestamps)
1971 );
1972 }
1973 if (snapshot1.hasFileHashes() || snapshot2.hasFileHashes()) {
1974 snapshot.setFileHashes(
1975 mergeMaps(snapshot1.fileHashes, snapshot2.fileHashes)
1976 );
1977 }
1978 if (snapshot1.hasFileTshs() || snapshot2.hasFileTshs()) {
1979 snapshot.setFileTshs(mergeMaps(snapshot1.fileTshs, snapshot2.fileTshs));
1980 }
1981 if (snapshot1.hasContextTimestamps() || snapshot2.hasContextTimestamps()) {
1982 snapshot.setContextTimestamps(
1983 mergeMaps(snapshot1.contextTimestamps, snapshot2.contextTimestamps)
1984 );
1985 }
1986 if (snapshot1.hasContextHashes() || snapshot2.hasContextHashes()) {
1987 snapshot.setContextHashes(
1988 mergeMaps(snapshot1.contextHashes, snapshot2.contextHashes)
1989 );
1990 }
1991 if (snapshot1.hasContextTshs() || snapshot2.hasContextTshs()) {
1992 snapshot.setContextTshs(
1993 mergeMaps(snapshot1.contextTshs, snapshot2.contextTshs)
1994 );
1995 }
1996 if (snapshot1.hasMissingExistence() || snapshot2.hasMissingExistence()) {
1997 snapshot.setMissingExistence(
1998 mergeMaps(snapshot1.missingExistence, snapshot2.missingExistence)
1999 );
2000 }
2001 if (snapshot1.hasManagedItemInfo() || snapshot2.hasManagedItemInfo()) {
2002 snapshot.setManagedItemInfo(
2003 mergeMaps(snapshot1.managedItemInfo, snapshot2.managedItemInfo)
2004 );
2005 }
2006 if (snapshot1.hasManagedFiles() || snapshot2.hasManagedFiles()) {
2007 snapshot.setManagedFiles(
2008 mergeSets(snapshot1.managedFiles, snapshot2.managedFiles)
2009 );
2010 }
2011 if (snapshot1.hasManagedContexts() || snapshot2.hasManagedContexts()) {
2012 snapshot.setManagedContexts(
2013 mergeSets(snapshot1.managedContexts, snapshot2.managedContexts)
2014 );
2015 }
2016 if (snapshot1.hasManagedMissing() || snapshot2.hasManagedMissing()) {
2017 snapshot.setManagedMissing(
2018 mergeSets(snapshot1.managedMissing, snapshot2.managedMissing)
2019 );
2020 }
2021 if (snapshot1.hasChildren() || snapshot2.hasChildren()) {
2022 snapshot.setChildren(mergeSets(snapshot1.children, snapshot2.children));
2023 }
2024 if (
2025 this._snapshotCache.get(snapshot1) === true &&
2026 this._snapshotCache.get(snapshot2) === true
2027 ) {
2028 this._snapshotCache.set(snapshot, true);
2029 }
2030 return snapshot;
2031 }
2032
2033 /**
2034 * @param {Snapshot} snapshot the snapshot made
2035 * @param {function(WebpackError=, boolean=): void} callback callback function
2036 * @returns {void}
2037 */
2038 checkSnapshotValid(snapshot, callback) {
2039 const cachedResult = this._snapshotCache.get(snapshot);
2040 if (cachedResult !== undefined) {
2041 this._statTestedSnapshotsCached++;
2042 if (typeof cachedResult === "boolean") {
2043 callback(null, cachedResult);
2044 } else {
2045 cachedResult.push(callback);
2046 }
2047 return;
2048 }
2049 this._statTestedSnapshotsNotCached++;
2050 this._checkSnapshotValidNoCache(snapshot, callback);
2051 }
2052
2053 /**
2054 * @param {Snapshot} snapshot the snapshot made
2055 * @param {function(WebpackError=, boolean=): void} callback callback function
2056 * @returns {void}
2057 */
2058 _checkSnapshotValidNoCache(snapshot, callback) {
2059 /** @type {number | undefined} */
2060 let startTime = undefined;
2061 if (snapshot.hasStartTime()) {
2062 startTime = snapshot.startTime;
2063 }
2064 let jobs = 1;
2065 const jobDone = () => {
2066 if (--jobs === 0) {
2067 this._snapshotCache.set(snapshot, true);
2068 callback(null, true);
2069 }
2070 };
2071 const invalid = () => {
2072 if (jobs > 0) {
2073 // large negative number instead of NaN or something else to keep jobs to stay a SMI (v8)
2074 jobs = -100000000;
2075 this._snapshotCache.set(snapshot, false);
2076 callback(null, false);
2077 }
2078 };
2079 const invalidWithError = (path, err) => {
2080 if (this._remainingLogs > 0) {
2081 this._log(path, `error occurred: %s`, err);
2082 }
2083 invalid();
2084 };
2085 /**
2086 * @param {string} path file path
2087 * @param {string} current current hash
2088 * @param {string} snap snapshot hash
2089 * @returns {boolean} true, if ok
2090 */
2091 const checkHash = (path, current, snap) => {
2092 if (current !== snap) {
2093 // If hash differ it's invalid
2094 if (this._remainingLogs > 0) {
2095 this._log(path, `hashes differ (%s != %s)`, current, snap);
2096 }
2097 return false;
2098 }
2099 return true;
2100 };
2101 /**
2102 * @param {string} path file path
2103 * @param {boolean} current current entry
2104 * @param {boolean} snap entry from snapshot
2105 * @returns {boolean} true, if ok
2106 */
2107 const checkExistence = (path, current, snap) => {
2108 if (!current !== !snap) {
2109 // If existence of item differs
2110 // it's invalid
2111 if (this._remainingLogs > 0) {
2112 this._log(
2113 path,
2114 current ? "it didn't exist before" : "it does no longer exist"
2115 );
2116 }
2117 return false;
2118 }
2119 return true;
2120 };
2121 /**
2122 * @param {string} path file path
2123 * @param {FileSystemInfoEntry} current current entry
2124 * @param {FileSystemInfoEntry} snap entry from snapshot
2125 * @param {boolean} log log reason
2126 * @returns {boolean} true, if ok
2127 */
2128 const checkFile = (path, current, snap, log = true) => {
2129 if (current === snap) return true;
2130 if (!current !== !snap) {
2131 // If existence of item differs
2132 // it's invalid
2133 if (log && this._remainingLogs > 0) {
2134 this._log(
2135 path,
2136 current ? "it didn't exist before" : "it does no longer exist"
2137 );
2138 }
2139 return false;
2140 }
2141 if (current) {
2142 // For existing items only
2143 if (typeof startTime === "number" && current.safeTime > startTime) {
2144 // If a change happened after starting reading the item
2145 // this may no longer be valid
2146 if (log && this._remainingLogs > 0) {
2147 this._log(
2148 path,
2149 `it may have changed (%d) after the start time of the snapshot (%d)`,
2150 current.safeTime,
2151 startTime
2152 );
2153 }
2154 return false;
2155 }
2156 if (
2157 snap.timestamp !== undefined &&
2158 current.timestamp !== snap.timestamp
2159 ) {
2160 // If we have a timestamp (it was a file or symlink) and it differs from current timestamp
2161 // it's invalid
2162 if (log && this._remainingLogs > 0) {
2163 this._log(
2164 path,
2165 `timestamps differ (%d != %d)`,
2166 current.timestamp,
2167 snap.timestamp
2168 );
2169 }
2170 return false;
2171 }
2172 if (
2173 snap.timestampHash !== undefined &&
2174 current.timestampHash !== snap.timestampHash
2175 ) {
2176 // If we have a timestampHash (it was a directory) and it differs from current timestampHash
2177 // it's invalid
2178 if (log && this._remainingLogs > 0) {
2179 this._log(
2180 path,
2181 `timestamps hashes differ (%s != %s)`,
2182 current.timestampHash,
2183 snap.timestampHash
2184 );
2185 }
2186 return false;
2187 }
2188 }
2189 return true;
2190 };
2191 if (snapshot.hasChildren()) {
2192 const childCallback = (err, result) => {
2193 if (err || !result) return invalid();
2194 else jobDone();
2195 };
2196 for (const child of snapshot.children) {
2197 const cache = this._snapshotCache.get(child);
2198 if (cache !== undefined) {
2199 this._statTestedChildrenCached++;
2200 /* istanbul ignore else */
2201 if (typeof cache === "boolean") {
2202 if (cache === false) {
2203 invalid();
2204 return;
2205 }
2206 } else {
2207 jobs++;
2208 cache.push(childCallback);
2209 }
2210 } else {
2211 this._statTestedChildrenNotCached++;
2212 jobs++;
2213 this._checkSnapshotValidNoCache(child, childCallback);
2214 }
2215 }
2216 }
2217 if (snapshot.hasFileTimestamps()) {
2218 const { fileTimestamps } = snapshot;
2219 this._statTestedEntries += fileTimestamps.size;
2220 for (const [path, ts] of fileTimestamps) {
2221 const cache = this._fileTimestamps.get(path);
2222 if (cache !== undefined) {
2223 if (cache !== "ignore" && !checkFile(path, cache, ts)) {
2224 invalid();
2225 return;
2226 }
2227 } else {
2228 jobs++;
2229 this.fileTimestampQueue.add(path, (err, entry) => {
2230 if (err) return invalidWithError(path, err);
2231 if (!checkFile(path, entry, ts)) {
2232 invalid();
2233 } else {
2234 jobDone();
2235 }
2236 });
2237 }
2238 }
2239 }
2240 const processFileHashSnapshot = (path, hash) => {
2241 const cache = this._fileHashes.get(path);
2242 if (cache !== undefined) {
2243 if (cache !== "ignore" && !checkHash(path, cache, hash)) {
2244 invalid();
2245 return;
2246 }
2247 } else {
2248 jobs++;
2249 this.fileHashQueue.add(path, (err, entry) => {
2250 if (err) return invalidWithError(path, err);
2251 if (!checkHash(path, entry, hash)) {
2252 invalid();
2253 } else {
2254 jobDone();
2255 }
2256 });
2257 }
2258 };
2259 if (snapshot.hasFileHashes()) {
2260 const { fileHashes } = snapshot;
2261 this._statTestedEntries += fileHashes.size;
2262 for (const [path, hash] of fileHashes) {
2263 processFileHashSnapshot(path, hash);
2264 }
2265 }
2266 if (snapshot.hasFileTshs()) {
2267 const { fileTshs } = snapshot;
2268 this._statTestedEntries += fileTshs.size;
2269 for (const [path, tsh] of fileTshs) {
2270 if (typeof tsh === "string") {
2271 processFileHashSnapshot(path, tsh);
2272 } else {
2273 const cache = this._fileTimestamps.get(path);
2274 if (cache !== undefined) {
2275 if (cache === "ignore" || !checkFile(path, cache, tsh, false)) {
2276 processFileHashSnapshot(path, tsh.hash);
2277 }
2278 } else {
2279 jobs++;
2280 this.fileTimestampQueue.add(path, (err, entry) => {
2281 if (err) return invalidWithError(path, err);
2282 if (!checkFile(path, entry, tsh, false)) {
2283 processFileHashSnapshot(path, tsh.hash);
2284 }
2285 jobDone();
2286 });
2287 }
2288 }
2289 }
2290 }
2291 if (snapshot.hasContextTimestamps()) {
2292 const { contextTimestamps } = snapshot;
2293 this._statTestedEntries += contextTimestamps.size;
2294 for (const [path, ts] of contextTimestamps) {
2295 const cache = this._contextTimestamps.get(path);
2296 if (cache !== undefined) {
2297 if (cache !== "ignore" && !checkFile(path, cache, ts)) {
2298 invalid();
2299 return;
2300 }
2301 } else {
2302 jobs++;
2303 this.contextTimestampQueue.add(path, (err, entry) => {
2304 if (err) return invalidWithError(path, err);
2305 if (!checkFile(path, entry, ts)) {
2306 invalid();
2307 } else {
2308 jobDone();
2309 }
2310 });
2311 }
2312 }
2313 }
2314 const processContextHashSnapshot = (path, hash) => {
2315 const cache = this._contextHashes.get(path);
2316 if (cache !== undefined) {
2317 if (cache !== "ignore" && !checkHash(path, cache, hash)) {
2318 invalid();
2319 return;
2320 }
2321 } else {
2322 jobs++;
2323 this.contextHashQueue.add(path, (err, entry) => {
2324 if (err) return invalidWithError(path, err);
2325 if (!checkHash(path, entry, hash)) {
2326 invalid();
2327 } else {
2328 jobDone();
2329 }
2330 });
2331 }
2332 };
2333 if (snapshot.hasContextHashes()) {
2334 const { contextHashes } = snapshot;
2335 this._statTestedEntries += contextHashes.size;
2336 for (const [path, hash] of contextHashes) {
2337 processContextHashSnapshot(path, hash);
2338 }
2339 }
2340 if (snapshot.hasContextTshs()) {
2341 const { contextTshs } = snapshot;
2342 this._statTestedEntries += contextTshs.size;
2343 for (const [path, tsh] of contextTshs) {
2344 if (typeof tsh === "string") {
2345 processContextHashSnapshot(path, tsh);
2346 } else {
2347 const cache = this._contextTimestamps.get(path);
2348 if (cache !== undefined) {
2349 if (cache === "ignore" || !checkFile(path, cache, tsh, false)) {
2350 processContextHashSnapshot(path, tsh.hash);
2351 }
2352 } else {
2353 jobs++;
2354 this.contextTimestampQueue.add(path, (err, entry) => {
2355 if (err) return invalidWithError(path, err);
2356 if (!checkFile(path, entry, tsh, false)) {
2357 processContextHashSnapshot(path, tsh.hash);
2358 }
2359 jobDone();
2360 });
2361 }
2362 }
2363 }
2364 }
2365 if (snapshot.hasMissingExistence()) {
2366 const { missingExistence } = snapshot;
2367 this._statTestedEntries += missingExistence.size;
2368 for (const [path, existence] of missingExistence) {
2369 const cache = this._fileTimestamps.get(path);
2370 if (cache !== undefined) {
2371 if (
2372 cache !== "ignore" &&
2373 !checkExistence(path, toExistence(cache), existence)
2374 ) {
2375 invalid();
2376 return;
2377 }
2378 } else {
2379 jobs++;
2380 this.fileTimestampQueue.add(path, (err, entry) => {
2381 if (err) return invalidWithError(path, err);
2382 if (!checkExistence(path, toExistence(entry), existence)) {
2383 invalid();
2384 } else {
2385 jobDone();
2386 }
2387 });
2388 }
2389 }
2390 }
2391 if (snapshot.hasManagedItemInfo()) {
2392 const { managedItemInfo } = snapshot;
2393 this._statTestedEntries += managedItemInfo.size;
2394 for (const [path, info] of managedItemInfo) {
2395 const cache = this._managedItems.get(path);
2396 if (cache !== undefined) {
2397 if (!checkHash(path, cache, info)) {
2398 invalid();
2399 return;
2400 }
2401 } else {
2402 jobs++;
2403 this.managedItemQueue.add(path, (err, entry) => {
2404 if (err) return invalidWithError(path, err);
2405 if (!checkHash(path, entry, info)) {
2406 invalid();
2407 } else {
2408 jobDone();
2409 }
2410 });
2411 }
2412 }
2413 }
2414 jobDone();
2415
2416 // if there was an async action
2417 // try to join multiple concurrent request for this snapshot
2418 if (jobs > 0) {
2419 const callbacks = [callback];
2420 callback = (err, result) => {
2421 for (const callback of callbacks) callback(err, result);
2422 };
2423 this._snapshotCache.set(snapshot, callbacks);
2424 }
2425 }
2426
2427 _readFileTimestamp(path, callback) {
2428 this.fs.stat(path, (err, stat) => {
2429 if (err) {
2430 if (err.code === "ENOENT") {
2431 this._fileTimestamps.set(path, null);
2432 this._cachedDeprecatedFileTimestamps = undefined;
2433 return callback(null, null);
2434 }
2435 return callback(err);
2436 }
2437
2438 let ts;
2439 if (stat.isDirectory()) {
2440 ts = {
2441 safeTime: 0,
2442 timestamp: undefined
2443 };
2444 } else {
2445 const mtime = +stat.mtime;
2446
2447 if (mtime) applyMtime(mtime);
2448
2449 ts = {
2450 safeTime: mtime ? mtime + FS_ACCURACY : Infinity,
2451 timestamp: mtime
2452 };
2453 }
2454
2455 this._fileTimestamps.set(path, ts);
2456 this._cachedDeprecatedFileTimestamps = undefined;
2457
2458 callback(null, ts);
2459 });
2460 }
2461
2462 _readFileHash(path, callback) {
2463 this.fs.readFile(path, (err, content) => {
2464 if (err) {
2465 if (err.code === "EISDIR") {
2466 this._fileHashes.set(path, "directory");
2467 return callback(null, "directory");
2468 }
2469 if (err.code === "ENOENT") {
2470 this._fileHashes.set(path, null);
2471 return callback(null, null);
2472 }
2473 if (err.code === "ERR_FS_FILE_TOO_LARGE") {
2474 this.logger.warn(`Ignoring ${path} for hashing as it's very large`);
2475 this._fileHashes.set(path, "too large");
2476 return callback(null, "too large");
2477 }
2478 return callback(err);
2479 }
2480
2481 const hash = createHash("md4");
2482
2483 hash.update(content);
2484
2485 const digest = /** @type {string} */ (hash.digest("hex"));
2486
2487 this._fileHashes.set(path, digest);
2488
2489 callback(null, digest);
2490 });
2491 }
2492
2493 _getFileTimestampAndHash(path, callback) {
2494 const continueWithHash = hash => {
2495 const cache = this._fileTimestamps.get(path);
2496 if (cache !== undefined) {
2497 if (cache !== "ignore") {
2498 const result = {
2499 ...cache,
2500 hash
2501 };
2502 this._fileTshs.set(path, result);
2503 return callback(null, result);
2504 } else {
2505 this._fileTshs.set(path, hash);
2506 return callback(null, hash);
2507 }
2508 } else {
2509 this.fileTimestampQueue.add(path, (err, entry) => {
2510 if (err) {
2511 return callback(err);
2512 }
2513 const result = {
2514 ...entry,
2515 hash
2516 };
2517 this._fileTshs.set(path, result);
2518 return callback(null, result);
2519 });
2520 }
2521 };
2522
2523 const cache = this._fileHashes.get(path);
2524 if (cache !== undefined) {
2525 continueWithHash(cache);
2526 } else {
2527 this.fileHashQueue.add(path, (err, entry) => {
2528 if (err) {
2529 return callback(err);
2530 }
2531 continueWithHash(entry);
2532 });
2533 }
2534 }
2535
2536 _readContextTimestamp(path, callback) {
2537 this.fs.readdir(path, (err, _files) => {
2538 if (err) {
2539 if (err.code === "ENOENT") {
2540 this._contextTimestamps.set(path, null);
2541 this._cachedDeprecatedContextTimestamps = undefined;
2542 return callback(null, null);
2543 }
2544 return callback(err);
2545 }
2546 const files = /** @type {string[]} */ (_files)
2547 .map(file => file.normalize("NFC"))
2548 .filter(file => !/^\./.test(file))
2549 .sort();
2550 asyncLib.map(
2551 files,
2552 (file, callback) => {
2553 const child = join(this.fs, path, file);
2554 this.fs.stat(child, (err, stat) => {
2555 if (err) return callback(err);
2556
2557 for (const immutablePath of this.immutablePathsWithSlash) {
2558 if (path.startsWith(immutablePath)) {
2559 // ignore any immutable path for timestamping
2560 return callback(null, null);
2561 }
2562 }
2563 for (const managedPath of this.managedPathsWithSlash) {
2564 if (path.startsWith(managedPath)) {
2565 const managedItem = getManagedItem(managedPath, child);
2566 if (managedItem) {
2567 // construct timestampHash from managed info
2568 return this.managedItemQueue.add(managedItem, (err, info) => {
2569 if (err) return callback(err);
2570 return callback(null, {
2571 safeTime: 0,
2572 timestampHash: info
2573 });
2574 });
2575 }
2576 }
2577 }
2578
2579 if (stat.isFile()) {
2580 return this.getFileTimestamp(child, callback);
2581 }
2582 if (stat.isDirectory()) {
2583 this.contextTimestampQueue.increaseParallelism();
2584 this.getContextTimestamp(child, (err, tsEntry) => {
2585 this.contextTimestampQueue.decreaseParallelism();
2586 callback(err, tsEntry);
2587 });
2588 return;
2589 }
2590 callback(null, null);
2591 });
2592 },
2593 (err, tsEntries) => {
2594 if (err) return callback(err);
2595 const hash = createHash("md4");
2596
2597 for (const file of files) hash.update(file);
2598 let safeTime = 0;
2599 for (const entry of tsEntries) {
2600 if (!entry) {
2601 hash.update("n");
2602 continue;
2603 }
2604 if (entry.timestamp) {
2605 hash.update("f");
2606 hash.update(`${entry.timestamp}`);
2607 } else if (entry.timestampHash) {
2608 hash.update("d");
2609 hash.update(`${entry.timestampHash}`);
2610 }
2611 if (entry.safeTime) {
2612 safeTime = Math.max(safeTime, entry.safeTime);
2613 }
2614 }
2615
2616 const digest = /** @type {string} */ (hash.digest("hex"));
2617
2618 const result = {
2619 safeTime,
2620 timestampHash: digest
2621 };
2622
2623 this._contextTimestamps.set(path, result);
2624 this._cachedDeprecatedContextTimestamps = undefined;
2625
2626 callback(null, result);
2627 }
2628 );
2629 });
2630 }
2631
2632 _readContextHash(path, callback) {
2633 this.fs.readdir(path, (err, _files) => {
2634 if (err) {
2635 if (err.code === "ENOENT") {
2636 this._contextHashes.set(path, null);
2637 return callback(null, null);
2638 }
2639 return callback(err);
2640 }
2641 const files = /** @type {string[]} */ (_files)
2642 .map(file => file.normalize("NFC"))
2643 .filter(file => !/^\./.test(file))
2644 .sort();
2645 asyncLib.map(
2646 files,
2647 (file, callback) => {
2648 const child = join(this.fs, path, file);
2649 this.fs.stat(child, (err, stat) => {
2650 if (err) return callback(err);
2651
2652 for (const immutablePath of this.immutablePathsWithSlash) {
2653 if (path.startsWith(immutablePath)) {
2654 // ignore any immutable path for hashing
2655 return callback(null, "");
2656 }
2657 }
2658 for (const managedPath of this.managedPathsWithSlash) {
2659 if (path.startsWith(managedPath)) {
2660 const managedItem = getManagedItem(managedPath, child);
2661 if (managedItem) {
2662 // construct hash from managed info
2663 return this.managedItemQueue.add(managedItem, (err, info) => {
2664 if (err) return callback(err);
2665 callback(null, info || "");
2666 });
2667 }
2668 }
2669 }
2670
2671 if (stat.isFile()) {
2672 return this.getFileHash(child, (err, hash) => {
2673 callback(err, hash || "");
2674 });
2675 }
2676 if (stat.isDirectory()) {
2677 this.contextHashQueue.increaseParallelism();
2678 this.getContextHash(child, (err, hash) => {
2679 this.contextHashQueue.decreaseParallelism();
2680 callback(err, hash || "");
2681 });
2682 return;
2683 }
2684 callback(null, "");
2685 });
2686 },
2687 (err, fileHashes) => {
2688 if (err) return callback(err);
2689 const hash = createHash("md4");
2690
2691 for (const file of files) hash.update(file);
2692 for (const h of fileHashes) hash.update(h);
2693
2694 const digest = /** @type {string} */ (hash.digest("hex"));
2695
2696 this._contextHashes.set(path, digest);
2697
2698 callback(null, digest);
2699 }
2700 );
2701 });
2702 }
2703
2704 _getContextTimestampAndHash(path, callback) {
2705 const continueWithHash = hash => {
2706 const cache = this._contextTimestamps.get(path);
2707 if (cache !== undefined) {
2708 if (cache !== "ignore") {
2709 const result = {
2710 ...cache,
2711 hash
2712 };
2713 this._contextTshs.set(path, result);
2714 return callback(null, result);
2715 } else {
2716 this._contextTshs.set(path, hash);
2717 return callback(null, hash);
2718 }
2719 } else {
2720 this.contextTimestampQueue.add(path, (err, entry) => {
2721 if (err) {
2722 return callback(err);
2723 }
2724 const result = {
2725 ...entry,
2726 hash
2727 };
2728 this._contextTshs.set(path, result);
2729 return callback(null, result);
2730 });
2731 }
2732 };
2733
2734 const cache = this._contextHashes.get(path);
2735 if (cache !== undefined) {
2736 continueWithHash(cache);
2737 } else {
2738 this.contextHashQueue.add(path, (err, entry) => {
2739 if (err) {
2740 return callback(err);
2741 }
2742 continueWithHash(entry);
2743 });
2744 }
2745 }
2746
2747 _getManagedItemDirectoryInfo(path, callback) {
2748 this.fs.readdir(path, (err, elements) => {
2749 if (err) {
2750 if (err.code === "ENOENT" || err.code === "ENOTDIR") {
2751 return callback(null, EMPTY_SET);
2752 }
2753 return callback(err);
2754 }
2755 const set = new Set(
2756 /** @type {string[]} */ (elements).map(element =>
2757 join(this.fs, path, element)
2758 )
2759 );
2760 callback(null, set);
2761 });
2762 }
2763
2764 _getManagedItemInfo(path, callback) {
2765 const dir = dirname(this.fs, path);
2766 this.managedItemDirectoryQueue.add(dir, (err, elements) => {
2767 if (err) {
2768 return callback(err);
2769 }
2770 if (!elements.has(path)) {
2771 // file or directory doesn't exist
2772 this._managedItems.set(path, "missing");
2773 return callback(null, "missing");
2774 }
2775 // something exists
2776 // it may be a file or directory
2777 if (
2778 path.endsWith("node_modules") &&
2779 (path.endsWith("/node_modules") || path.endsWith("\\node_modules"))
2780 ) {
2781 // we are only interested in existence of this special directory
2782 this._managedItems.set(path, "exists");
2783 return callback(null, "exists");
2784 }
2785
2786 // we assume it's a directory, as files shouldn't occur in managed paths
2787 const packageJsonPath = join(this.fs, path, "package.json");
2788 this.fs.readFile(packageJsonPath, (err, content) => {
2789 if (err) {
2790 if (err.code === "ENOENT" || err.code === "ENOTDIR") {
2791 // no package.json or path is not a directory
2792 this.fs.readdir(path, (err, elements) => {
2793 if (
2794 !err &&
2795 elements.length === 1 &&
2796 elements[0] === "node_modules"
2797 ) {
2798 // This is only a grouping folder e. g. used by yarn
2799 // we are only interested in existence of this special directory
2800 this._managedItems.set(path, "nested");
2801 return callback(null, "nested");
2802 }
2803 const problem = `Managed item ${path} isn't a directory or doesn't contain a package.json`;
2804 this.logger.warn(problem);
2805 return callback(new Error(problem));
2806 });
2807 return;
2808 }
2809 return callback(err);
2810 }
2811 let data;
2812 try {
2813 data = JSON.parse(content.toString("utf-8"));
2814 } catch (e) {
2815 return callback(e);
2816 }
2817 const info = `${data.name || ""}@${data.version || ""}`;
2818 this._managedItems.set(path, info);
2819 callback(null, info);
2820 });
2821 });
2822 }
2823
2824 getDeprecatedFileTimestamps() {
2825 if (this._cachedDeprecatedFileTimestamps !== undefined)
2826 return this._cachedDeprecatedFileTimestamps;
2827 const map = new Map();
2828 for (const [path, info] of this._fileTimestamps) {
2829 if (info) map.set(path, typeof info === "object" ? info.safeTime : null);
2830 }
2831 return (this._cachedDeprecatedFileTimestamps = map);
2832 }
2833
2834 getDeprecatedContextTimestamps() {
2835 if (this._cachedDeprecatedContextTimestamps !== undefined)
2836 return this._cachedDeprecatedContextTimestamps;
2837 const map = new Map();
2838 for (const [path, info] of this._contextTimestamps) {
2839 if (info) map.set(path, typeof info === "object" ? info.safeTime : null);
2840 }
2841 return (this._cachedDeprecatedContextTimestamps = map);
2842 }
2843}
2844
2845module.exports = FileSystemInfo;
2846module.exports.Snapshot = Snapshot;