UNPKG

webpack

Version:

Packs ECMAScript/CommonJs/AMD modules for the browser. Allows you to split your codebase into multiple bundles, which can be loaded on demand. Supports loaders to preprocess files, i.e. json, jsx, es7, css, less, ... and your custom stuff.

1,663 lines (1,546 loc) 125 kB
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const nodeModule = require("module"); const { isAbsolute } = require("path"); const { create: createResolver } = require("enhanced-resolve"); const asyncLib = require("neo-async"); const { DEFAULTS } = require("./config/defaults"); const AsyncQueue = require("./util/AsyncQueue"); const StackedCacheMap = require("./util/StackedCacheMap"); const createHash = require("./util/createHash"); const { dirname, join, lstatReadlinkAbsolute, relative } = require("./util/fs"); const makeSerializable = require("./util/makeSerializable"); const memoize = require("./util/memoize"); const processAsyncTree = require("./util/processAsyncTree"); /** @typedef {import("enhanced-resolve").ResolveRequest} ResolveRequest */ /** @typedef {import("enhanced-resolve").ResolveFunctionAsync} ResolveFunctionAsync */ /** @typedef {import("./WebpackError")} WebpackError */ /** @typedef {import("./logging/Logger").Logger} Logger */ /** @typedef {import("./serialization/ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */ /** @typedef {import("./serialization/ObjectMiddleware").ObjectSerializerContext} ObjectSerializerContext */ /** @typedef {import("../declarations/WebpackOptions").HashFunction} HashFunction */ /** @typedef {import("./util/fs").JsonObject} JsonObject */ /** @typedef {import("./util/fs").IStats} IStats */ /** @typedef {import("./util/fs").InputFileSystem} InputFileSystem */ /** * Defines the processor callback type used by this module. * @template T * @typedef {import("./util/AsyncQueue").Callback<T>} ProcessorCallback */ /** * Defines the processor type used by this module. * @template T, R * @typedef {import("./util/AsyncQueue").Processor<T, R>} Processor */ const supportsEsm = Number(process.versions.modules) >= 83; /** @type {Set<string>} */ const builtinModules = new Set(nodeModule.builtinModules); let FS_ACCURACY = 2000; /** @type {Set<string>} */ const EMPTY_SET = new Set(); const RBDT_RESOLVE_INITIAL = 0; const RBDT_RESOLVE_FILE = 1; const RBDT_RESOLVE_DIRECTORY = 2; const RBDT_RESOLVE_CJS_FILE = 3; const RBDT_RESOLVE_CJS_FILE_AS_CHILD = 4; const RBDT_RESOLVE_ESM_FILE = 5; const RBDT_DIRECTORY = 6; const RBDT_FILE = 7; const RBDT_DIRECTORY_DEPENDENCIES = 8; const RBDT_FILE_DEPENDENCIES = 9; /** @typedef {RBDT_RESOLVE_INITIAL | RBDT_RESOLVE_FILE | RBDT_RESOLVE_DIRECTORY | RBDT_RESOLVE_CJS_FILE | RBDT_RESOLVE_CJS_FILE_AS_CHILD | RBDT_RESOLVE_ESM_FILE | RBDT_DIRECTORY | RBDT_FILE | RBDT_DIRECTORY_DEPENDENCIES | RBDT_FILE_DEPENDENCIES} JobType */ const INVALID = Symbol("invalid"); // eslint-disable-next-line jsdoc/ts-no-empty-object-type /** @typedef {{ }} ExistenceOnlyTimeEntry */ /** * Defines the file system info entry type used by this module. * @typedef {object} FileSystemInfoEntry * @property {number} safeTime * @property {number=} timestamp */ /** * Defines the resolved context file system info entry type used by this module. * @typedef {object} ResolvedContextFileSystemInfoEntry * @property {number} safeTime * @property {string=} timestampHash */ /** @typedef {Set<string>} Symlinks */ /** * Defines the context file system info entry type used by this module. * @typedef {object} ContextFileSystemInfoEntry * @property {number} safeTime * @property {string=} timestampHash * @property {ResolvedContextFileSystemInfoEntry=} resolved * @property {Symlinks=} symlinks */ /** * Defines the timestamp and hash type used by this module. * @typedef {object} TimestampAndHash * @property {number} safeTime * @property {number=} timestamp * @property {string} hash */ /** * Defines the resolved context timestamp and hash type used by this module. * @typedef {object} ResolvedContextTimestampAndHash * @property {number} safeTime * @property {string=} timestampHash * @property {string} hash */ /** * Defines the context timestamp and hash type used by this module. * @typedef {object} ContextTimestampAndHash * @property {number} safeTime * @property {string=} timestampHash * @property {string} hash * @property {ResolvedContextTimestampAndHash=} resolved * @property {Symlinks=} symlinks */ /** * Defines the context hash type used by this module. * @typedef {object} ContextHash * @property {string} hash * @property {string=} resolved * @property {Symlinks=} symlinks */ /** @typedef {Set<string>} SnapshotContent */ /** * Defines the snapshot optimization entry type used by this module. * @typedef {object} SnapshotOptimizationEntry * @property {Snapshot} snapshot * @property {number} shared * @property {SnapshotContent | undefined} snapshotContent * @property {Set<SnapshotOptimizationEntry> | undefined} children */ /** @typedef {Map<string, string | false | undefined>} ResolveResults */ /** @typedef {Set<string>} Files */ /** @typedef {Set<string>} Directories */ /** @typedef {Set<string>} Missing */ /** * Defines the resolve dependencies type used by this module. * @typedef {object} ResolveDependencies * @property {Files} files list of files * @property {Directories} directories list of directories * @property {Missing} missing list of missing entries */ /** * Defines the resolve build dependencies result type used by this module. * @typedef {object} ResolveBuildDependenciesResult * @property {Files} files list of files * @property {Directories} directories list of directories * @property {Missing} missing list of missing entries * @property {ResolveResults} resolveResults stored resolve results * @property {ResolveDependencies} resolveDependencies dependencies of the resolving */ /** * Defines the snapshot options type used by this module. * @typedef {object} SnapshotOptions * @property {boolean=} hash should use hash to snapshot * @property {boolean=} timestamp should use timestamp to snapshot */ const DONE_ITERATOR_RESULT = new Set().keys().next(); // cspell:word tshs // Tsh = Timestamp + Hash // Tshs = Timestamp + Hash combinations class SnapshotIterator { /** * Creates an instance of SnapshotIterator. * @param {() => IteratorResult<string>} next next */ constructor(next) { this.next = next; } } /** * Defines the get maps function type used by this module. * @template T * @typedef {(snapshot: Snapshot) => T[]} GetMapsFunction */ /** * Represents SnapshotIterable. * @template T */ class SnapshotIterable { /** * Creates an instance of SnapshotIterable. * @param {Snapshot} snapshot snapshot * @param {GetMapsFunction<T>} getMaps get maps function */ constructor(snapshot, getMaps) { this.snapshot = snapshot; this.getMaps = getMaps; } [Symbol.iterator]() { let state = 0; /** @type {IterableIterator<string>} */ let it; /** @type {GetMapsFunction<T>} */ let getMaps; /** @type {T[]} */ let maps; /** @type {Snapshot} */ let snapshot; /** @type {Snapshot[] | undefined} */ let queue; return new SnapshotIterator(() => { for (;;) { switch (state) { case 0: snapshot = this.snapshot; getMaps = this.getMaps; maps = getMaps(snapshot); state = 1; /* falls through */ case 1: if (maps.length > 0) { const map = maps.pop(); if (map !== undefined) { it = /** @type {Set<EXPECTED_ANY> | Map<string, EXPECTED_ANY>} */ (map).keys(); state = 2; } else { break; } } else { state = 3; break; } /* falls through */ case 2: { const result = it.next(); if (!result.done) return result; state = 1; break; } case 3: { const children = snapshot.children; if (children !== undefined) { if (children.size === 1) { // shortcut for a single child // avoids allocation of queue for (const child of children) snapshot = child; maps = getMaps(snapshot); state = 1; break; } if (queue === undefined) queue = []; for (const child of children) { queue.push(child); } } if (queue !== undefined && queue.length > 0) { snapshot = /** @type {Snapshot} */ (queue.pop()); maps = getMaps(snapshot); state = 1; break; } else { state = 4; } } /* falls through */ case 4: return DONE_ITERATOR_RESULT; } } }); } } /** @typedef {Map<string, FileSystemInfoEntry | null>} FileTimestamps */ /** @typedef {Map<string, string | null>} FileHashes */ /** @typedef {Map<string, TimestampAndHash | string | null>} FileTshs */ /** @typedef {Map<string, ResolvedContextFileSystemInfoEntry | null>} ContextTimestamps */ /** @typedef {Map<string, string | null>} ContextHashes */ /** @typedef {Map<string, ResolvedContextTimestampAndHash | null>} ContextTshs */ /** @typedef {Map<string, boolean>} MissingExistence */ /** @typedef {Map<string, string>} ManagedItemInfo */ /** @typedef {Set<string>} ManagedFiles */ /** @typedef {Set<string>} ManagedContexts */ /** @typedef {Set<string>} ManagedMissing */ /** @typedef {Set<Snapshot>} Children */ class Snapshot { constructor() { this._flags = 0; /** @type {Iterable<string> | undefined} */ this._cachedFileIterable = undefined; /** @type {Iterable<string> | undefined} */ this._cachedContextIterable = undefined; /** @type {Iterable<string> | undefined} */ this._cachedMissingIterable = undefined; /** @type {number | undefined} */ this.startTime = undefined; /** @type {FileTimestamps | undefined} */ this.fileTimestamps = undefined; /** @type {FileHashes | undefined} */ this.fileHashes = undefined; /** @type {FileTshs | undefined} */ this.fileTshs = undefined; /** @type {ContextTimestamps | undefined} */ this.contextTimestamps = undefined; /** @type {ContextHashes | undefined} */ this.contextHashes = undefined; /** @type {ContextTshs | undefined} */ this.contextTshs = undefined; /** @type {MissingExistence | undefined} */ this.missingExistence = undefined; /** @type {ManagedItemInfo | undefined} */ this.managedItemInfo = undefined; /** @type {ManagedFiles | undefined} */ this.managedFiles = undefined; /** @type {ManagedContexts | undefined} */ this.managedContexts = undefined; /** @type {ManagedMissing | undefined} */ this.managedMissing = undefined; /** @type {Children | undefined} */ this.children = undefined; } hasStartTime() { return (this._flags & 1) !== 0; } /** * Updates start time using the provided value. * @param {number} value start value */ setStartTime(value) { this._flags |= 1; this.startTime = value; } /** * Sets merged start time. * @param {number | undefined} value value * @param {Snapshot} snapshot snapshot */ setMergedStartTime(value, snapshot) { if (value) { if (snapshot.hasStartTime()) { this.setStartTime( Math.min( value, /** @type {NonNullable<Snapshot["startTime"]>} */ (snapshot.startTime) ) ); } else { this.setStartTime(value); } } else if (snapshot.hasStartTime()) { this.setStartTime( /** @type {NonNullable<Snapshot["startTime"]>} */ (snapshot.startTime) ); } } hasFileTimestamps() { return (this._flags & 2) !== 0; } /** * Sets file timestamps. * @param {FileTimestamps} value file timestamps */ setFileTimestamps(value) { this._flags |= 2; this.fileTimestamps = value; } hasFileHashes() { return (this._flags & 4) !== 0; } /** * Updates file hashes using the provided value. * @param {FileHashes} value file hashes */ setFileHashes(value) { this._flags |= 4; this.fileHashes = value; } hasFileTshs() { return (this._flags & 8) !== 0; } /** * Updates file tshs using the provided value. * @param {FileTshs} value file tshs */ setFileTshs(value) { this._flags |= 8; this.fileTshs = value; } hasContextTimestamps() { return (this._flags & 0x10) !== 0; } /** * Sets context timestamps. * @param {ContextTimestamps} value context timestamps */ setContextTimestamps(value) { this._flags |= 0x10; this.contextTimestamps = value; } hasContextHashes() { return (this._flags & 0x20) !== 0; } /** * Sets context hashes. * @param {ContextHashes} value context hashes */ setContextHashes(value) { this._flags |= 0x20; this.contextHashes = value; } hasContextTshs() { return (this._flags & 0x40) !== 0; } /** * Updates context tshs using the provided value. * @param {ContextTshs} value context tshs */ setContextTshs(value) { this._flags |= 0x40; this.contextTshs = value; } hasMissingExistence() { return (this._flags & 0x80) !== 0; } /** * Sets missing existence. * @param {MissingExistence} value context tshs */ setMissingExistence(value) { this._flags |= 0x80; this.missingExistence = value; } hasManagedItemInfo() { return (this._flags & 0x100) !== 0; } /** * Sets managed item info. * @param {ManagedItemInfo} value managed item info */ setManagedItemInfo(value) { this._flags |= 0x100; this.managedItemInfo = value; } hasManagedFiles() { return (this._flags & 0x200) !== 0; } /** * Sets managed files. * @param {ManagedFiles} value managed files */ setManagedFiles(value) { this._flags |= 0x200; this.managedFiles = value; } hasManagedContexts() { return (this._flags & 0x400) !== 0; } /** * Sets managed contexts. * @param {ManagedContexts} value managed contexts */ setManagedContexts(value) { this._flags |= 0x400; this.managedContexts = value; } hasManagedMissing() { return (this._flags & 0x800) !== 0; } /** * Sets managed missing. * @param {ManagedMissing} value managed missing */ setManagedMissing(value) { this._flags |= 0x800; this.managedMissing = value; } hasChildren() { return (this._flags & 0x1000) !== 0; } /** * Updates children using the provided value. * @param {Children} value children */ setChildren(value) { this._flags |= 0x1000; this.children = value; } /** * Adds the provided child to the snapshot. * @param {Snapshot} child children */ addChild(child) { if (!this.hasChildren()) { this.setChildren(new Set()); } /** @type {Children} */ (this.children).add(child); } /** * Serializes this instance into the provided serializer context. * @param {ObjectSerializerContext} context context */ serialize({ write }) { write(this._flags); if (this.hasStartTime()) write(this.startTime); if (this.hasFileTimestamps()) write(this.fileTimestamps); if (this.hasFileHashes()) write(this.fileHashes); if (this.hasFileTshs()) write(this.fileTshs); if (this.hasContextTimestamps()) write(this.contextTimestamps); if (this.hasContextHashes()) write(this.contextHashes); if (this.hasContextTshs()) write(this.contextTshs); if (this.hasMissingExistence()) write(this.missingExistence); if (this.hasManagedItemInfo()) write(this.managedItemInfo); if (this.hasManagedFiles()) write(this.managedFiles); if (this.hasManagedContexts()) write(this.managedContexts); if (this.hasManagedMissing()) write(this.managedMissing); if (this.hasChildren()) write(this.children); } /** * Restores this instance from the provided deserializer context. * @param {ObjectDeserializerContext} context context */ deserialize({ read }) { this._flags = read(); if (this.hasStartTime()) this.startTime = read(); if (this.hasFileTimestamps()) this.fileTimestamps = read(); if (this.hasFileHashes()) this.fileHashes = read(); if (this.hasFileTshs()) this.fileTshs = read(); if (this.hasContextTimestamps()) this.contextTimestamps = read(); if (this.hasContextHashes()) this.contextHashes = read(); if (this.hasContextTshs()) this.contextTshs = read(); if (this.hasMissingExistence()) this.missingExistence = read(); if (this.hasManagedItemInfo()) this.managedItemInfo = read(); if (this.hasManagedFiles()) this.managedFiles = read(); if (this.hasManagedContexts()) this.managedContexts = read(); if (this.hasManagedMissing()) this.managedMissing = read(); if (this.hasChildren()) this.children = read(); } /** * Creates an iterable from the provided get map. * @template T * @param {GetMapsFunction<T>} getMaps first * @returns {SnapshotIterable<T>} iterable */ _createIterable(getMaps) { return new SnapshotIterable(this, getMaps); } /** * Gets file iterable. * @returns {Iterable<string>} iterable */ getFileIterable() { if (this._cachedFileIterable === undefined) { this._cachedFileIterable = this._createIterable((s) => [ s.fileTimestamps, s.fileHashes, s.fileTshs, s.managedFiles ]); } return this._cachedFileIterable; } /** * Gets context iterable. * @returns {Iterable<string>} iterable */ getContextIterable() { if (this._cachedContextIterable === undefined) { this._cachedContextIterable = this._createIterable((s) => [ s.contextTimestamps, s.contextHashes, s.contextTshs, s.managedContexts ]); } return this._cachedContextIterable; } /** * Gets missing iterable. * @returns {Iterable<string>} iterable */ getMissingIterable() { if (this._cachedMissingIterable === undefined) { this._cachedMissingIterable = this._createIterable((s) => [ s.missingExistence, s.managedMissing ]); } return this._cachedMissingIterable; } } makeSerializable(Snapshot, "webpack/lib/FileSystemInfo", "Snapshot"); const MIN_COMMON_SNAPSHOT_SIZE = 3; /** * Defines the snapshot optimization value type used by this module. * @template U, T * @typedef {U extends true ? Set<string> : Map<string, T>} SnapshotOptimizationValue */ /** * Represents SnapshotOptimization. * @template T * @template {boolean} [U=false] */ class SnapshotOptimization { /** * Creates an instance of SnapshotOptimization. * @param {(snapshot: Snapshot) => boolean} has has value * @param {(snapshot: Snapshot) => SnapshotOptimizationValue<U, T> | undefined} get get value * @param {(snapshot: Snapshot, value: SnapshotOptimizationValue<U, T>) => void} set set value * @param {boolean=} useStartTime use the start time of snapshots * @param {U=} isSet value is an Set instead of a Map */ constructor( has, get, set, useStartTime = true, isSet = /** @type {U} */ (false) ) { this._has = has; this._get = get; this._set = set; this._useStartTime = useStartTime; /** @type {U} */ this._isSet = isSet; /** @type {Map<string, SnapshotOptimizationEntry>} */ this._map = new Map(); this._statItemsShared = 0; this._statItemsUnshared = 0; this._statSharedSnapshots = 0; this._statReusedSharedSnapshots = 0; } getStatisticMessage() { const total = this._statItemsShared + this._statItemsUnshared; if (total === 0) return; return `${ this._statItemsShared && Math.round((this._statItemsShared * 100) / total) }% (${this._statItemsShared}/${total}) entries shared via ${ this._statSharedSnapshots } shared snapshots (${ this._statReusedSharedSnapshots + this._statSharedSnapshots } times referenced)`; } clear() { this._map.clear(); this._statItemsShared = 0; this._statItemsUnshared = 0; this._statSharedSnapshots = 0; this._statReusedSharedSnapshots = 0; } /** * Processes the provided new snapshot. * @param {Snapshot} newSnapshot snapshot * @param {Set<string>} capturedFiles files to snapshot/share * @returns {void} */ optimize(newSnapshot, capturedFiles) { if (capturedFiles.size === 0) { return; } /** * Increase shared and store optimization entry. * @param {SnapshotOptimizationEntry} entry optimization entry * @returns {void} */ const increaseSharedAndStoreOptimizationEntry = (entry) => { if (entry.children !== undefined) { for (const child of entry.children) { increaseSharedAndStoreOptimizationEntry(child); } } entry.shared++; storeOptimizationEntry(entry); }; /** * Stores optimization entry. * @param {SnapshotOptimizationEntry} entry optimization entry * @returns {void} */ const storeOptimizationEntry = (entry) => { for (const path of /** @type {SnapshotContent} */ ( entry.snapshotContent )) { const old = /** @type {SnapshotOptimizationEntry} */ (this._map.get(path)); if (old.shared < entry.shared) { this._map.set(path, entry); } capturedFiles.delete(path); } }; /** @type {SnapshotOptimizationEntry | undefined} */ let newOptimizationEntry; const capturedFilesSize = capturedFiles.size; /** @type {Set<SnapshotOptimizationEntry> | undefined} */ const optimizationEntries = new Set(); for (const path of capturedFiles) { const optimizationEntry = this._map.get(path); if (optimizationEntry === undefined) { if (newOptimizationEntry === undefined) { newOptimizationEntry = { snapshot: newSnapshot, shared: 0, snapshotContent: undefined, children: undefined }; } this._map.set(path, newOptimizationEntry); } else { optimizationEntries.add(optimizationEntry); } } optimizationEntriesLabel: for (const optimizationEntry of optimizationEntries) { const snapshot = optimizationEntry.snapshot; if (optimizationEntry.shared > 0) { // It's a shared snapshot // We can't change it, so we can only use it when all files match // and startTime is compatible if ( this._useStartTime && newSnapshot.startTime && (!snapshot.startTime || snapshot.startTime > newSnapshot.startTime) ) { continue; } /** @type {Set<string>} */ const nonSharedFiles = new Set(); const snapshotContent = /** @type {NonNullable<SnapshotOptimizationEntry["snapshotContent"]>} */ (optimizationEntry.snapshotContent); const snapshotEntries = /** @type {SnapshotOptimizationValue<U, T>} */ (this._get(snapshot)); for (const path of snapshotContent) { if (!capturedFiles.has(path)) { if (!snapshotEntries.has(path)) { // File is not shared and can't be removed from the snapshot // because it's in a child of the snapshot continue optimizationEntriesLabel; } nonSharedFiles.add(path); } } if (nonSharedFiles.size === 0) { // The complete snapshot is shared // add it as child newSnapshot.addChild(snapshot); increaseSharedAndStoreOptimizationEntry(optimizationEntry); this._statReusedSharedSnapshots++; } else { // Only a part of the snapshot is shared const sharedCount = snapshotContent.size - nonSharedFiles.size; if (sharedCount < MIN_COMMON_SNAPSHOT_SIZE) { // Common part it too small continue; } // Extract common timestamps from both snapshots /** @type {Set<string> | Map<string, T>} */ let commonMap; if (this._isSet) { commonMap = new Set(); for (const path of /** @type {Set<string>} */ (snapshotEntries)) { if (nonSharedFiles.has(path)) continue; commonMap.add(path); snapshotEntries.delete(path); } } else { commonMap = new Map(); const map = /** @type {Map<string, T>} */ (snapshotEntries); for (const [path, value] of map) { if (nonSharedFiles.has(path)) continue; commonMap.set(path, value); snapshotEntries.delete(path); } } // Create and attach snapshot const commonSnapshot = new Snapshot(); if (this._useStartTime) { commonSnapshot.setMergedStartTime(newSnapshot.startTime, snapshot); } this._set( commonSnapshot, /** @type {SnapshotOptimizationValue<U, T>} */ (commonMap) ); newSnapshot.addChild(commonSnapshot); snapshot.addChild(commonSnapshot); // Create optimization entry const newEntry = { snapshot: commonSnapshot, shared: optimizationEntry.shared + 1, snapshotContent: new Set(commonMap.keys()), children: undefined }; if (optimizationEntry.children === undefined) { optimizationEntry.children = new Set(); } optimizationEntry.children.add(newEntry); storeOptimizationEntry(newEntry); this._statSharedSnapshots++; } } else { // It's a unshared snapshot // We can extract a common shared snapshot // with all common files const snapshotEntries = this._get(snapshot); if (snapshotEntries === undefined) { // Incomplete snapshot, that can't be used continue; } /** @type {Set<string> | Map<string, T>} */ let commonMap; if (this._isSet) { commonMap = new Set(); const set = /** @type {Set<string>} */ (snapshotEntries); if (capturedFiles.size < set.size) { for (const path of capturedFiles) { if (set.has(path)) commonMap.add(path); } } else { for (const path of set) { if (capturedFiles.has(path)) commonMap.add(path); } } } else { commonMap = new Map(); const map = /** @type {Map<string, T>} */ (snapshotEntries); for (const path of capturedFiles) { const ts = map.get(path); if (ts === undefined) continue; commonMap.set(path, ts); } } if (commonMap.size < MIN_COMMON_SNAPSHOT_SIZE) { // Common part it too small continue; } // Create and attach snapshot const commonSnapshot = new Snapshot(); if (this._useStartTime) { commonSnapshot.setMergedStartTime(newSnapshot.startTime, snapshot); } this._set( commonSnapshot, /** @type {SnapshotOptimizationValue<U, T>} */ (commonMap) ); newSnapshot.addChild(commonSnapshot); snapshot.addChild(commonSnapshot); // Remove files from snapshot for (const path of commonMap.keys()) snapshotEntries.delete(path); const sharedCount = commonMap.size; this._statItemsUnshared -= sharedCount; this._statItemsShared += sharedCount; // Create optimization entry storeOptimizationEntry({ snapshot: commonSnapshot, shared: 2, snapshotContent: new Set(commonMap.keys()), children: undefined }); this._statSharedSnapshots++; } } const unshared = capturedFiles.size; this._statItemsUnshared += unshared; this._statItemsShared += capturedFilesSize - unshared; } } /** * Returns result. * @param {string} str input * @returns {string} result */ const parseString = (str) => { if (str[0] === "'" || str[0] === "`") { str = `"${str.slice(1, -1).replace(/"/g, '\\"')}"`; } return JSON.parse(str); }; /* istanbul ignore next */ /** * Processes the provided mtime. * @param {number} mtime mtime */ const applyMtime = (mtime) => { if (FS_ACCURACY > 1 && mtime % 2 !== 0) FS_ACCURACY = 1; else if (FS_ACCURACY > 10 && mtime % 20 !== 0) FS_ACCURACY = 10; else if (FS_ACCURACY > 100 && mtime % 200 !== 0) FS_ACCURACY = 100; else if (FS_ACCURACY > 1000 && mtime % 2000 !== 0) FS_ACCURACY = 1000; }; /** * Merges the provided values into a single result. * @template T * @template K * @param {Map<T, K> | undefined} a source map * @param {Map<T, K> | undefined} b joining map * @returns {Map<T, K>} joined map */ const mergeMaps = (a, b) => { if (!b || b.size === 0) return /** @type {Map<T, K>} */ (a); if (!a || a.size === 0) return /** @type {Map<T, K>} */ (b); /** @type {Map<T, K>} */ const map = new Map(a); for (const [key, value] of b) { map.set(key, value); } return map; }; /** * Merges the provided values into a single result. * @template T * @param {Set<T> | undefined} a source map * @param {Set<T> | undefined} b joining map * @returns {Set<T>} joined map */ const mergeSets = (a, b) => { if (!b || b.size === 0) return /** @type {Set<T>} */ (a); if (!a || a.size === 0) return /** @type {Set<T>} */ (b); /** @type {Set<T>} */ const map = new Set(a); for (const item of b) { map.add(item); } return map; }; /** * Finding file or directory to manage * @param {string} managedPath path that is managing by {@link FileSystemInfo} * @param {string} path path to file or directory * @returns {string | null} managed item * @example * getManagedItem( * '/Users/user/my-project/node_modules/', * '/Users/user/my-project/node_modules/package/index.js' * ) === '/Users/user/my-project/node_modules/package' * getManagedItem( * '/Users/user/my-project/node_modules/', * '/Users/user/my-project/node_modules/package1/node_modules/package2' * ) === '/Users/user/my-project/node_modules/package1/node_modules/package2' * getManagedItem( * '/Users/user/my-project/node_modules/', * '/Users/user/my-project/node_modules/.bin/script.js' * ) === null // hidden files are disallowed as managed items * getManagedItem( * '/Users/user/my-project/node_modules/', * '/Users/user/my-project/node_modules/package' * ) === '/Users/user/my-project/node_modules/package' */ const getManagedItem = (managedPath, path) => { let i = managedPath.length; let slashes = 1; let startingPosition = true; loop: while (i < path.length) { switch (path.charCodeAt(i)) { case 47: // slash case 92: // backslash if (--slashes === 0) break loop; startingPosition = true; break; case 46: // . // hidden files are disallowed as managed items // it's probably .yarn-integrity or .cache if (startingPosition) return null; break; case 64: // @ if (!startingPosition) return null; slashes++; break; default: startingPosition = false; break; } i++; } if (i === path.length) slashes--; // return null when path is incomplete if (slashes !== 0) return null; // if (path.slice(i + 1, i + 13) === "node_modules") if ( path.length >= i + 13 && path.charCodeAt(i + 1) === 110 && path.charCodeAt(i + 2) === 111 && path.charCodeAt(i + 3) === 100 && path.charCodeAt(i + 4) === 101 && path.charCodeAt(i + 5) === 95 && path.charCodeAt(i + 6) === 109 && path.charCodeAt(i + 7) === 111 && path.charCodeAt(i + 8) === 100 && path.charCodeAt(i + 9) === 117 && path.charCodeAt(i + 10) === 108 && path.charCodeAt(i + 11) === 101 && path.charCodeAt(i + 12) === 115 ) { // if this is the end of the path if (path.length === i + 13) { // return the node_modules directory // it's special return path; } const c = path.charCodeAt(i + 13); // if next symbol is slash or backslash if (c === 47 || c === 92) { // Managed subpath return getManagedItem(path.slice(0, i + 14), path); } } return path.slice(0, i); }; /** * Gets resolved timestamp. * @template {ContextFileSystemInfoEntry | ContextTimestampAndHash} T * @param {T | null} entry entry * @returns {T["resolved"] | null | undefined} the resolved entry */ const getResolvedTimestamp = (entry) => { if (entry === null) return null; if (entry.resolved !== undefined) return entry.resolved; return entry.symlinks === undefined ? entry : undefined; }; /** * Gets resolved hash. * @param {ContextHash | null} entry entry * @returns {string | null | undefined} the resolved entry */ const getResolvedHash = (entry) => { if (entry === null) return null; if (entry.resolved !== undefined) return entry.resolved; return entry.symlinks === undefined ? entry.hash : undefined; }; /** * Adds the provided source to the snapshot optimization. * @template T * @param {Set<T>} source source * @param {Set<T>} target target */ const addAll = (source, target) => { for (const key of source) target.add(key); }; const getEsModuleLexer = memoize(() => require("es-module-lexer")); /** @typedef {Set<string>} LoggedPaths */ /** @typedef {FileSystemInfoEntry | "ignore" | null} FileTimestamp */ /** @typedef {ContextFileSystemInfoEntry | "ignore" | null} ContextTimestamp */ /** @typedef {ResolvedContextFileSystemInfoEntry | "ignore" | null} ResolvedContextTimestamp */ /** @typedef {(err?: WebpackError | null, result?: boolean) => void} CheckSnapshotValidCallback */ /** * Used to access information about the filesystem in a cached way */ class FileSystemInfo { /** * Creates an instance of FileSystemInfo. * @param {InputFileSystem} fs file system * @param {object} options options * @param {Iterable<string | RegExp>=} options.unmanagedPaths paths that are not managed by a package manager and the contents are subject to change * @param {Iterable<string | RegExp>=} options.managedPaths paths that are only managed by a package manager * @param {Iterable<string | RegExp>=} options.immutablePaths paths that are immutable * @param {Logger=} options.logger logger used to log invalid snapshots * @param {HashFunction=} options.hashFunction the hash function to use */ constructor( fs, { unmanagedPaths = [], managedPaths = [], immutablePaths = [], logger, hashFunction = DEFAULTS.HASH_FUNCTION } = {} ) { this.fs = fs; this.logger = logger; this._remainingLogs = logger ? 40 : 0; /** @type {LoggedPaths | undefined} */ this._loggedPaths = logger ? new Set() : undefined; this._hashFunction = hashFunction; /** @type {WeakMap<Snapshot, boolean | CheckSnapshotValidCallback[]>} */ this._snapshotCache = new WeakMap(); this._fileTimestampsOptimization = new SnapshotOptimization( (s) => s.hasFileTimestamps(), (s) => s.fileTimestamps, (s, v) => s.setFileTimestamps(v) ); this._fileHashesOptimization = new SnapshotOptimization( (s) => s.hasFileHashes(), (s) => s.fileHashes, (s, v) => s.setFileHashes(v), false ); this._fileTshsOptimization = new SnapshotOptimization( (s) => s.hasFileTshs(), (s) => s.fileTshs, (s, v) => s.setFileTshs(v) ); this._contextTimestampsOptimization = new SnapshotOptimization( (s) => s.hasContextTimestamps(), (s) => s.contextTimestamps, (s, v) => s.setContextTimestamps(v) ); this._contextHashesOptimization = new SnapshotOptimization( (s) => s.hasContextHashes(), (s) => s.contextHashes, (s, v) => s.setContextHashes(v), false ); this._contextTshsOptimization = new SnapshotOptimization( (s) => s.hasContextTshs(), (s) => s.contextTshs, (s, v) => s.setContextTshs(v) ); this._missingExistenceOptimization = new SnapshotOptimization( (s) => s.hasMissingExistence(), (s) => s.missingExistence, (s, v) => s.setMissingExistence(v), false ); this._managedItemInfoOptimization = new SnapshotOptimization( (s) => s.hasManagedItemInfo(), (s) => s.managedItemInfo, (s, v) => s.setManagedItemInfo(v), false ); this._managedFilesOptimization = new SnapshotOptimization( (s) => s.hasManagedFiles(), (s) => s.managedFiles, (s, v) => s.setManagedFiles(v), false, true ); this._managedContextsOptimization = new SnapshotOptimization( (s) => s.hasManagedContexts(), (s) => s.managedContexts, (s, v) => s.setManagedContexts(v), false, true ); this._managedMissingOptimization = new SnapshotOptimization( (s) => s.hasManagedMissing(), (s) => s.managedMissing, (s, v) => s.setManagedMissing(v), false, true ); /** @type {StackedCacheMap<string, FileTimestamp>} */ this._fileTimestamps = new StackedCacheMap(); /** @type {Map<string, string | null>} */ this._fileHashes = new Map(); /** @type {Map<string, TimestampAndHash | string>} */ this._fileTshs = new Map(); /** @type {StackedCacheMap<string, ContextTimestamp>} */ this._contextTimestamps = new StackedCacheMap(); /** @type {Map<string, ContextHash>} */ this._contextHashes = new Map(); /** @type {Map<string, ContextTimestampAndHash>} */ this._contextTshs = new Map(); /** @type {Map<string, string>} */ this._managedItems = new Map(); /** @type {AsyncQueue<string, string, FileSystemInfoEntry>} */ this.fileTimestampQueue = new AsyncQueue({ name: "file timestamp", parallelism: 30, processor: this._readFileTimestamp.bind(this) }); /** @type {AsyncQueue<string, string, string>} */ this.fileHashQueue = new AsyncQueue({ name: "file hash", parallelism: 10, processor: this._readFileHash.bind(this) }); /** @type {AsyncQueue<string, string, ContextFileSystemInfoEntry>} */ this.contextTimestampQueue = new AsyncQueue({ name: "context timestamp", parallelism: 2, processor: this._readContextTimestamp.bind(this) }); /** @type {AsyncQueue<string, string, ContextHash>} */ this.contextHashQueue = new AsyncQueue({ name: "context hash", parallelism: 2, processor: this._readContextHash.bind(this) }); /** @type {AsyncQueue<string, string, ContextTimestampAndHash>} */ this.contextTshQueue = new AsyncQueue({ name: "context hash and timestamp", parallelism: 2, processor: this._readContextTimestampAndHash.bind(this) }); /** @type {AsyncQueue<string, string, string>} */ this.managedItemQueue = new AsyncQueue({ name: "managed item info", parallelism: 10, processor: this._getManagedItemInfo.bind(this) }); /** @type {AsyncQueue<string, string, Set<string>>} */ this.managedItemDirectoryQueue = new AsyncQueue({ name: "managed item directory info", parallelism: 10, processor: this._getManagedItemDirectoryInfo.bind(this) }); const _unmanagedPaths = [...unmanagedPaths]; /** @type {string[]} */ this.unmanagedPathsWithSlash = _unmanagedPaths .filter((p) => typeof p === "string") .map((p) => join(fs, p, "_").slice(0, -1)); /** @type {RegExp[]} */ this.unmanagedPathsRegExps = _unmanagedPaths.filter( (p) => typeof p !== "string" ); this.managedPaths = [...managedPaths]; /** @type {string[]} */ this.managedPathsWithSlash = this.managedPaths .filter((p) => typeof p === "string") .map((p) => join(fs, p, "_").slice(0, -1)); /** @type {RegExp[]} */ this.managedPathsRegExps = this.managedPaths.filter( (p) => typeof p !== "string" ); this.immutablePaths = [...immutablePaths]; /** @type {string[]} */ this.immutablePathsWithSlash = this.immutablePaths .filter((p) => typeof p === "string") .map((p) => join(fs, p, "_").slice(0, -1)); /** @type {RegExp[]} */ this.immutablePathsRegExps = this.immutablePaths.filter( (p) => typeof p !== "string" ); this._cachedDeprecatedFileTimestamps = undefined; this._cachedDeprecatedContextTimestamps = undefined; this._warnAboutExperimentalEsmTracking = false; this._statCreatedSnapshots = 0; this._statTestedSnapshotsCached = 0; this._statTestedSnapshotsNotCached = 0; this._statTestedChildrenCached = 0; this._statTestedChildrenNotCached = 0; this._statTestedEntries = 0; } logStatistics() { const logger = /** @type {Logger} */ (this.logger); /** * Processes the provided header. * @param {string} header header * @param {string | undefined} message message */ const logWhenMessage = (header, message) => { if (message) { logger.log(`${header}: ${message}`); } }; logger.log(`${this._statCreatedSnapshots} new snapshots created`); logger.log( `${ this._statTestedSnapshotsNotCached && Math.round( (this._statTestedSnapshotsNotCached * 100) / (this._statTestedSnapshotsCached + this._statTestedSnapshotsNotCached) ) }% root snapshot uncached (${this._statTestedSnapshotsNotCached} / ${ this._statTestedSnapshotsCached + this._statTestedSnapshotsNotCached })` ); logger.log( `${ this._statTestedChildrenNotCached && Math.round( (this._statTestedChildrenNotCached * 100) / (this._statTestedChildrenCached + this._statTestedChildrenNotCached) ) }% children snapshot uncached (${this._statTestedChildrenNotCached} / ${ this._statTestedChildrenCached + this._statTestedChildrenNotCached })` ); logger.log(`${this._statTestedEntries} entries tested`); logger.log( `File info in cache: ${this._fileTimestamps.size} timestamps ${this._fileHashes.size} hashes ${this._fileTshs.size} timestamp hash combinations` ); logWhenMessage( "File timestamp snapshot optimization", this._fileTimestampsOptimization.getStatisticMessage() ); logWhenMessage( "File hash snapshot optimization", this._fileHashesOptimization.getStatisticMessage() ); logWhenMessage( "File timestamp hash combination snapshot optimization", this._fileTshsOptimization.getStatisticMessage() ); logger.log( `Directory info in cache: ${this._contextTimestamps.size} timestamps ${this._contextHashes.size} hashes ${this._contextTshs.size} timestamp hash combinations` ); logWhenMessage( "Directory timestamp snapshot optimization", this._contextTimestampsOptimization.getStatisticMessage() ); logWhenMessage( "Directory hash snapshot optimization", this._contextHashesOptimization.getStatisticMessage() ); logWhenMessage( "Directory timestamp hash combination snapshot optimization", this._contextTshsOptimization.getStatisticMessage() ); logWhenMessage( "Missing items snapshot optimization", this._missingExistenceOptimization.getStatisticMessage() ); logger.log(`Managed items info in cache: ${this._managedItems.size} items`); logWhenMessage( "Managed items snapshot optimization", this._managedItemInfoOptimization.getStatisticMessage() ); logWhenMessage( "Managed files snapshot optimization", this._managedFilesOptimization.getStatisticMessage() ); logWhenMessage( "Managed contexts snapshot optimization", this._managedContextsOptimization.getStatisticMessage() ); logWhenMessage( "Managed missing snapshot optimization", this._managedMissingOptimization.getStatisticMessage() ); } /** * Processes the provided path. * @private * @param {string} path path * @param {string} reason reason * @param {EXPECTED_ANY[]} args arguments */ _log(path, reason, ...args) { const key = path + reason; const loggedPaths = /** @type {LoggedPaths} */ (this._loggedPaths); if (loggedPaths.has(key)) return; loggedPaths.add(key); /** @type {Logger} */ (this.logger).debug(`${path} invalidated because ${reason}`, ...args); if (--this._remainingLogs === 0) { /** @type {Logger} */ (this.logger).debug( "Logging limit has been reached and no further logging will be emitted by FileSystemInfo" ); } } clear() { this._remainingLogs = this.logger ? 40 : 0; if (this._loggedPaths !== undefined) this._loggedPaths.clear(); this._snapshotCache = new WeakMap(); this._fileTimestampsOptimization.clear(); this._fileHashesOptimization.clear(); this._fileTshsOptimization.clear(); this._contextTimestampsOptimization.clear(); this._contextHashesOptimization.clear(); this._contextTshsOptimization.clear(); this._missingExistenceOptimization.clear(); this._managedItemInfoOptimization.clear(); this._managedFilesOptimization.clear(); this._managedContextsOptimization.clear(); this._managedMissingOptimization.clear(); this._fileTimestamps.clear(); this._fileHashes.clear(); this._fileTshs.clear(); this._contextTimestamps.clear(); this._contextHashes.clear(); this._contextTshs.clear(); this._managedItems.clear(); this._managedItems.clear(); this._cachedDeprecatedFileTimestamps = undefined; this._cachedDeprecatedContextTimestamps = undefined; this._statCreatedSnapshots = 0; this._statTestedSnapshotsCached = 0; this._statTestedSnapshotsNotCached = 0; this._statTestedChildrenCached = 0; this._statTestedChildrenNotCached = 0; this._statTestedEntries = 0; } /** * Adds file timestamps. * @param {ReadonlyMap<string, FileSystemInfoEntry | ExistenceOnlyTimeEntry | "ignore" | null>} map timestamps * @param {boolean=} immutable if 'map' is immutable and FileSystemInfo can keep referencing it * @returns {void} */ addFileTimestamps(map, immutable) { this._fileTimestamps.addAll( /** @type {ReadonlyMap<string, FileTimestamp>} */ (map), immutable ); this._cachedDeprecatedFileTimestamps = undefined; } /** * Adds context timestamps. * @param {ReadonlyMap<string, ContextFileSystemInfoEntry | ExistenceOnlyTimeEntry | "ignore" | null>} map timestamps * @param {boolean=} immutable if 'map' is immutable and FileSystemInfo can keep referencing it * @returns {void} */ addContextTimestamps(map, immutable) { this._contextTimestamps.addAll( /** @type {ReadonlyMap<string, ContextTimestamp>} */ (map), immutable ); this._cachedDeprecatedContextTimestamps = undefined; } /** * Gets file timestamp. * @param {string} path file path * @param {(err?: WebpackError | null, fileTimestamp?: FileTimestamp) => void} callback callback function * @returns {void} */ getFileTimestamp(path, callback) { const cache = this._fileTimestamps.get(path); if (cache !== undefined) return callback(null, cache); this.fileTimestampQueue.add(path, callback); } /** * Gets context timestamp. * @param {string} path context path * @param {(err?: WebpackError | null, resolvedContextTimestamp?: ResolvedContextTimestamp) => void} callback callback function * @returns {void} */ getContextTimestamp(path, callback) { const cache = this._contextTimestamps.get(path); if (cache !== undefined) { if (cache === "ignore") return callback(null, "ignore"); const resolved = getResolvedTimestamp(cache); if (resolved !== undefined) return callback(null, resolved); return this._resolveContextTimestamp( /** @type {ResolvedContextFileSystemInfoEntry} */ (cache), callback ); } this.contextTimestampQueue.add(path, (err, _entry) => { if (err) return callback(err); const entry = /** @type {ContextFileSystemInfoEntry} */ (_entry); const resolved = getResolvedTimestamp(entry); if (resolved !== undefined) return callback(null, resolved); this._resolveContextTimestamp(entry, callback); }); } /** * Get unresolved context timestamp. * @private * @param {string} path context path * @param {(err?: WebpackError | null, contextTimestamp?: ContextTimestamp) => void} callback callback function * @returns {void} */ _getUnresolvedContextTimestamp(path, callback) { const cache = this._contextTimestamps.get(path); if (cache !== undefined) return callback(null, cache); this.contextTimestampQueue.add(path, callback); } /** * Returns file hash. * @param {string} path file path * @param {(err?: WebpackError | null, hash?: string | null) => void} callback callback function * @returns {void} */ getFileHash(path, callback) { const cache = this._fileHashes.get(path); if (cache !== undefined) return callback(null, cache); this.fileHashQueue.add(path, callback); } /** * Returns context hash. * @param {string} path context path * @param {(err?: WebpackError | null, contextHash?: string) => void} callback callback function * @returns {void} */ getContextHash(path, callback) { const cache = this._contextHashes.get(path); if (cache !== undefined) { const resolved = getResolvedHash(cache); if (resolved !== undefined) { return callback(null, /** @type {string} */ (resolved)); } return this._resolveContextHash(cache, callback); } this.contextHashQueue.add(path, (err, _entry) => { if (err) return callback(err); const entry = /** @type {ContextHash} */ (_entry); const resolved = getResolvedHash(entry); if (resolved !== undefined) { return callback(null, /** @type {string} */ (resolved)); } this._resolveContextHash(entry, callback); }); } /** * Get unresolved context hash. * @private * @param {string} path context path * @param {(err?: WebpackError | null, contextHash?: ContextHash | null) => void} callback callback function * @returns {void} */ _getUnresolvedContextHash(path, callback) { const cache = this._contextHashes.get(path); if (cache !== undefined) return callback(null, cache); this.contextHashQueue.add(path, callback); } /** * Returns context tsh. * @param {string} path context path * @param {(err?: WebpackError | null, resolvedContextTimestampAndHash?: ResolvedContextTimestampAndHash | null) => void} callback callback function * @returns {void} */ getContextTsh(path, callback) { const cache = this._contextTshs.get(path); if (cache !== undefined) { const resolved = getResolvedTimestamp(cache); if (resolved !== undefined) return callback(null, resolved); return this._resolveContextTsh(cache, callback); } this.contextTshQueue.add(path, (err, _entry) => { if (err) return callback(err); const entry = /** @type {ContextTimestampAndHash} */ (_entry); const resolved = getResolvedTimestamp(entry); if (resolved !== undefined) return callback(null, resolved); this._resolveContextTsh(entry, callback); }); } /** * Get unresolved context tsh. * @private * @param {string} path context path * @param {(err?: WebpackError | null, contextTimestampAndHash?: ContextTimestampAndHash | null) => void} callback callback function * @returns {void} */ _getUnresolvedContextTsh(path, callback) { const cache = this._contextTshs.get(path); if (cache !== undefined) return callback(null, cache); this.contextTshQueue.add(path, callback); } _createBuildDependenciesResolvers() { const resolveContext = createResolver({ resolveToContext: true, exportsFields: [], fileSystem: this.fs }); const resolveCjs = createResolver({ extensions: [".js", ".json", ".node"], conditionNames: ["require", "node"], exportsFields: ["exports"], fileSystem: this.fs }); const resolveCjsAsChild = createResolver({ extensions: [".js", ".json", ".node"], conditionNames: ["require", "node"], exportsFields: [], fileSystem: this.fs }); const resolveEsm = createResolver({ extensions: [".js", ".json", ".node"], fullySpecified: true, conditionNames: ["import", "node"], exportsFields: ["exports"], fileSystem: this.fs }); return { resolveContext, resolveEsm, resolveCjs, resolveCjsAsChild }; } /** * Resolves build dependencies. * @param {string} context context directory * @param {Iterable<string>