UNPKG

react-carousel-query

Version:

A infinite carousel component made with react that handles the pagination for you.

1,659 lines (1,546 loc) 119 kB
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const { create: createResolver } = require("enhanced-resolve"); const nodeModule = require("module"); const asyncLib = require("neo-async"); const { isAbsolute } = require("path"); const AsyncQueue = require("./util/AsyncQueue"); const StackedCacheMap = require("./util/StackedCacheMap"); const createHash = require("./util/createHash"); const { join, dirname, relative, lstatReadlinkAbsolute } = require("./util/fs"); const makeSerializable = require("./util/makeSerializable"); const processAsyncTree = require("./util/processAsyncTree"); /** @typedef {import("enhanced-resolve").Resolver} Resolver */ /** @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 {typeof import("./util/Hash")} Hash */ /** @typedef {import("./util/fs").IStats} IStats */ /** @typedef {import("./util/fs").InputFileSystem} InputFileSystem */ /** @typedef {import("./util/fs").PathLike} PathLike */ /** @typedef {import("./util/fs").StringCallback} StringCallback */ /** * @template T * @typedef {import("./util/AsyncQueue").Callback<T>} ProcessorCallback */ /** * @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; const EMPTY_SET = new Set(); const RBDT_RESOLVE_CJS = 0; const RBDT_RESOLVE_ESM = 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_CJS | RBDT_RESOLVE_ESM | 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"); /** * @typedef {object} FileSystemInfoEntry * @property {number} safeTime * @property {number=} timestamp */ /** * @typedef {object} ResolvedContextFileSystemInfoEntry * @property {number} safeTime * @property {string=} timestampHash */ /** * @typedef {object} ContextFileSystemInfoEntry * @property {number} safeTime * @property {string=} timestampHash * @property {ResolvedContextFileSystemInfoEntry=} resolved * @property {Set<string>=} symlinks */ /** * @typedef {object} TimestampAndHash * @property {number} safeTime * @property {number=} timestamp * @property {string} hash */ /** * @typedef {object} ResolvedContextTimestampAndHash * @property {number} safeTime * @property {string=} timestampHash * @property {string} hash */ /** @typedef {Set<string>} Symlinks */ /** * @typedef {object} ContextTimestampAndHash * @property {number} safeTime * @property {string=} timestampHash * @property {string} hash * @property {ResolvedContextTimestampAndHash=} resolved * @property {Symlinks=} symlinks */ /** * @typedef {object} ContextHash * @property {string} hash * @property {string=} resolved * @property {Symlinks=} symlinks */ /** @typedef {Set<string>} SnapshotContent */ /** * @typedef {object} SnapshotOptimizationEntry * @property {Snapshot} snapshot * @property {number} shared * @property {SnapshotContent | undefined} snapshotContent * @property {Set<SnapshotOptimizationEntry> | undefined} children */ /** * @typedef {object} ResolveBuildDependenciesResult * @property {Set<string>} files list of files * @property {Set<string>} directories list of directories * @property {Set<string>} missing list of missing entries * @property {Map<string, string | false | undefined>} resolveResults stored resolve results * @property {object} resolveDependencies dependencies of the resolving * @property {Set<string>} resolveDependencies.files list of files * @property {Set<string>} resolveDependencies.directories list of directories * @property {Set<string>} resolveDependencies.missing list of missing entries */ /** * @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 { /** * @param {() => IteratorResult<string>} next next */ constructor(next) { this.next = next; } } /** * @typedef {(snapshot: Snapshot) => (Map<string, any> | Set<string> | undefined)[]} GetMapsFunction */ class SnapshotIterable { /** * @param {Snapshot} snapshot snapshot * @param {GetMapsFunction} getMaps get maps function */ constructor(snapshot, getMaps) { this.snapshot = snapshot; this.getMaps = getMaps; } [Symbol.iterator]() { let state = 0; /** @type {IterableIterator<string>} */ let it; /** @type {(snapshot: Snapshot) => (Map<string, any> | Set<string> | undefined)[]} */ let getMaps; /** @type {(Map<string, any> | Set<string> | undefined)[]} */ 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 = 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; } /** * @param {number} value start value */ setStartTime(value) { this._flags = this._flags | 1; this.startTime = value; } /** * @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; } /** * @param {FileTimestamps} value file timestamps */ setFileTimestamps(value) { this._flags = this._flags | 2; this.fileTimestamps = value; } hasFileHashes() { return (this._flags & 4) !== 0; } /** * @param {FileHashes} value file hashes */ setFileHashes(value) { this._flags = this._flags | 4; this.fileHashes = value; } hasFileTshs() { return (this._flags & 8) !== 0; } /** * @param {FileTshs} value file tshs */ setFileTshs(value) { this._flags = this._flags | 8; this.fileTshs = value; } hasContextTimestamps() { return (this._flags & 0x10) !== 0; } /** * @param {ContextTimestamps} value context timestamps */ setContextTimestamps(value) { this._flags = this._flags | 0x10; this.contextTimestamps = value; } hasContextHashes() { return (this._flags & 0x20) !== 0; } /** * @param {ContextHashes} value context hashes */ setContextHashes(value) { this._flags = this._flags | 0x20; this.contextHashes = value; } hasContextTshs() { return (this._flags & 0x40) !== 0; } /** * @param {ContextTshs} value context tshs */ setContextTshs(value) { this._flags = this._flags | 0x40; this.contextTshs = value; } hasMissingExistence() { return (this._flags & 0x80) !== 0; } /** * @param {MissingExistence} value context tshs */ setMissingExistence(value) { this._flags = this._flags | 0x80; this.missingExistence = value; } hasManagedItemInfo() { return (this._flags & 0x100) !== 0; } /** * @param {ManagedItemInfo} value managed item info */ setManagedItemInfo(value) { this._flags = this._flags | 0x100; this.managedItemInfo = value; } hasManagedFiles() { return (this._flags & 0x200) !== 0; } /** * @param {ManagedFiles} value managed files */ setManagedFiles(value) { this._flags = this._flags | 0x200; this.managedFiles = value; } hasManagedContexts() { return (this._flags & 0x400) !== 0; } /** * @param {ManagedContexts} value managed contexts */ setManagedContexts(value) { this._flags = this._flags | 0x400; this.managedContexts = value; } hasManagedMissing() { return (this._flags & 0x800) !== 0; } /** * @param {ManagedMissing} value managed missing */ setManagedMissing(value) { this._flags = this._flags | 0x800; this.managedMissing = value; } hasChildren() { return (this._flags & 0x1000) !== 0; } /** * @param {Children} value children */ setChildren(value) { this._flags = this._flags | 0x1000; this.children = value; } /** * @param {Snapshot} child children */ addChild(child) { if (!this.hasChildren()) { this.setChildren(new Set()); } /** @type {Children} */ (this.children).add(child); } /** * @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); } /** * @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(); } /** * @param {GetMapsFunction} getMaps first * @returns {Iterable<string>} iterable */ _createIterable(getMaps) { return new SnapshotIterable(this, getMaps); } /** * @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; } /** * @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; } /** * @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; /** * @template U, T * @typedef {U extends true ? Set<string> : Map<string, T>} SnapshotOptimizationValue */ /** * @template T * @template {boolean} [U=false] */ class SnapshotOptimization { /** * @param {function(Snapshot): boolean} has has value * @param {function(Snapshot): SnapshotOptimizationValue<U, T> | undefined} get get value * @param {function(Snapshot, 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; } /** * @param {Snapshot} newSnapshot snapshot * @param {Set<string>} capturedFiles files to snapshot/share * @returns {void} */ optimize(newSnapshot, capturedFiles) { /** * @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); }; /** * @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); continue; } 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; } 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); continue; } } 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 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; } 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; } } /** * @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 */ /** * @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; }; /** * @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; }; /** * @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); }; /** * @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; }; /** * @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; }; /** * @template T * @param {Set<T>} source source * @param {Set<T>} target target */ const addAll = (source, target) => { for (const key of source) target.add(key); }; /** @typedef {Set<string>} LoggedPaths */ /** * Used to access information about the filesystem in a cached way */ class 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 {string | Hash=} options.hashFunction the hash function to use */ constructor( fs, { unmanagedPaths = [], managedPaths = [], immutablePaths = [], logger, hashFunction = "md4" } = {} ) { 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 | (function((WebpackError | null)=, boolean=): void)[]>} */ 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, FileSystemInfoEntry | "ignore" | null>} */ 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, ContextFileSystemInfoEntry | "ignore" | null>} */ 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 = Array.from(unmanagedPaths); this.unmanagedPathsWithSlash = /** @type {string[]} */ ( _unmanagedPaths.filter(p => typeof p === "string") ).map(p => join(fs, p, "_").slice(0, -1)); this.unmanagedPathsRegExps = /** @type {RegExp[]} */ ( _unmanagedPaths.filter(p => typeof p !== "string") ); this.managedPaths = Array.from(managedPaths); this.managedPathsWithSlash = /** @type {string[]} */ ( this.managedPaths.filter(p => typeof p === "string") ).map(p => join(fs, p, "_").slice(0, -1)); this.managedPathsRegExps = /** @type {RegExp[]} */ ( this.managedPaths.filter(p => typeof p !== "string") ); this.immutablePaths = Array.from(immutablePaths); this.immutablePathsWithSlash = /** @type {string[]} */ ( this.immutablePaths.filter(p => typeof p === "string") ).map(p => join(fs, p, "_").slice(0, -1)); this.immutablePathsRegExps = /** @type {RegExp[]} */ ( 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); /** * @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() ); } /** * @param {string} path path * @param {string} reason reason * @param {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; } /** * @param {ReadonlyMap<string, FileSystemInfoEntry | "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(map, immutable); this._cachedDeprecatedFileTimestamps = undefined; } /** * @param {ReadonlyMap<string, FileSystemInfoEntry | "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(map, immutable); this._cachedDeprecatedContextTimestamps = undefined; } /** * @param {string} path file path * @param {function((WebpackError | null)=, (FileSystemInfoEntry | "ignore" | null)=): 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); } /** * @param {string} path context path * @param {function((WebpackError | null)=, (ResolvedContextFileSystemInfoEntry | "ignore" | null)=): 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); }); } /** * @param {string} path context path * @param {function((WebpackError | null)=, (ContextFileSystemInfoEntry | "ignore" | null)=): 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); } /** * @param {string} path file path * @param {function((WebpackError | null)=, (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); } /** * @param {string} path context path * @param {function((WebpackError | null)=, 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); }); } /** * @param {string} path context path * @param {function((WebpackError | null)=, (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); } /** * @param {string} path context path * @param {function((WebpackError | null)=, (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); }); } /** * @param {string} path context path * @param {function((WebpackError | null)=, (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 }; } /** * @param {string} context context directory * @param {Iterable<string>} deps dependencies * @param {function((Error | null)=, ResolveBuildDependenciesResult=): void} callback callback function * @returns {void} */ resolveBuildDependencies(context, deps, callback) { const { resolveContext, resolveEsm, resolveCjs, resolveCjsAsChild } = this._createBuildDependenciesResolvers(); /** @type {Set<string>} */ const files = new Set(); /** @type {Set<string>} */ const fileSymlinks = new Set(); /** @type {Set<string>} */ const directories = new Set(); /** @type {Set<string>} */ const directorySymlinks = new Set(); /** @type {Set<string>} */ const missing = new Set(); /** @type {Set<string>} */ const resolveFiles = new Set(); /** @type {Set<string>} */ const resolveDirectories = new Set(); /** @type {Set<string>} */ const resolveMissing = new Set(); /** @type {Map<string, string | false | undefined>} */ const resolveResults = new Map(); const invalidResolveResults = new Set(); const resolverContext = { fileDependencies: resolveFiles, contextDependencies: resolveDirectories, missingDependencies: resolveMissing }; /** * @param {undefined | boolean | string} expected expected result * @returns {string} expected result */ const expectedToString = expected => expected ? ` (expected ${expected})` : ""; /** @typedef {{ type: JobType, context: string | undefined, path: string, issuer: Job | undefined, expected: undefined | boolean | string }} Job */ /** * @param {Job} job job * @returns {`resolve commonjs file ${string}${string}`|`resolve esm file ${string}${string}`|`resolve esm ${string}${string}`|`resolve directory ${string}`|`file ${string}`|`unknown ${string} ${string}`|`resolve commonjs ${string}${string}`|`directory ${string}`|`file dependencies ${string}`|`directory dependencies ${string}`} result */ const jobToString = job => { switch (job.type) { case RBDT_RESOLVE_CJS: return `resolve commonjs ${job.path}${expectedToString( job.expected )}`; case RBDT_RESOLVE_ESM: return `resolve esm ${job.path}${expectedToString(job.expected)}`; case RBDT_RESOLVE_DIRECTORY: return `resolve directory ${job.path}`; case RBDT_RESOLVE_CJS_FILE: return `resolve commonjs file ${job.path}${expectedToString( job.expected )}`; case RBDT_RESOLVE_ESM_FILE: return `resolve esm file ${job.path}${expectedToString( job.expected )}`; case RBDT_DIRECTORY: return `directory ${job.path}`; case RBDT_FILE: return `file ${job.path}`; case RBDT_DIRECTORY_DEPENDENCIES: return `directory dependencies ${job.path}`; case RBDT_FILE_DEPENDENCIES: return `file dependencies ${job.path}`; } return `unknown ${job.type} ${job.path}`; }; /** * @param {Job} job job * @returns {string} string value */ const pathToString = job => { let result = ` at ${jobToString(job)}`; /** @type {Job | undefined} */ (job) = job.issuer; while (job !== undefined) { result += `\n at ${jobToString(job)}`; job = /** @type {Job} */ (job.issuer); } return result; }; const logger = /** @type {Logger} */ (this.logger); processAsyncTree( Array.from( deps, dep => /** @type {Job} */ ({ type: RBDT_RESOLVE_CJS, context, path: dep, expected: undefined, issuer: undefined }) ), 20, (job, push, callback) => { const { type, context, path, expected } = job; /** * @param {string} path path * @returns {void} */ const resolveDirectory = path => { const key = `d\n${context}\n${path}`; if (resolveResults.has(key)) { return callback(); } resolveResults.set(key, undefined); resolveContext( /** @type {string} */ (context), path,