file-entry-cache
Advanced tools
+290
| import { FlatCache, FlatCacheOptions } from "flat-cache"; | ||
| import { Buffer } from "node:buffer"; | ||
| //#region src/index.d.ts | ||
| type ILogger = { | ||
| /** Current log level */level?: string; /** Trace level logging */ | ||
| trace: (message: string | object, ...args: unknown[]) => void; /** Debug level logging */ | ||
| debug: (message: string | object, ...args: unknown[]) => void; /** Info level logging */ | ||
| info: (message: string | object, ...args: unknown[]) => void; /** Warning level logging */ | ||
| warn: (message: string | object, ...args: unknown[]) => void; /** Error level logging */ | ||
| error: (message: string | object, ...args: unknown[]) => void; /** Fatal level logging */ | ||
| fatal: (message: string | object, ...args: unknown[]) => void; | ||
| }; | ||
| type FileEntryCacheOptions = { | ||
| /** Whether to use file modified time for change detection (default: true) */useModifiedTime?: boolean; /** Whether to use checksum for change detection (default: false) */ | ||
| useCheckSum?: boolean; /** Hash algorithm to use for checksum (default: 'md5') */ | ||
| hashAlgorithm?: string; /** Current working directory for resolving relative paths (default: process.cwd()) */ | ||
| cwd?: string; /** Restrict file access to within cwd boundaries (default: true) */ | ||
| restrictAccessToCwd?: boolean; /** Whether to use absolute path as cache key (default: false) */ | ||
| useAbsolutePathAsKey?: boolean; /** Logger instance for logging (default: undefined) */ | ||
| logger?: ILogger; /** Options for the underlying flat cache */ | ||
| cache?: FlatCacheOptions; | ||
| }; | ||
| type GetFileDescriptorOptions = { | ||
| /** Whether to use checksum for this specific file check instead of modified time (mtime) (overrides instance setting) */useCheckSum?: boolean; /** Whether to use file modified time for change detection (default: true) */ | ||
| useModifiedTime?: boolean; | ||
| }; | ||
| type FileDescriptor = { | ||
| /** The cache key for this file (typically the file path) */key: string; /** Whether the file has changed since last cache check */ | ||
| changed?: boolean; /** Metadata about the file */ | ||
| meta: FileDescriptorMeta; /** Whether the file was not found */ | ||
| notFound?: boolean; /** Error encountered when accessing the file */ | ||
| err?: Error; | ||
| }; | ||
| type FileDescriptorMeta = { | ||
| /** File size in bytes */size?: number; /** File modification time (timestamp in milliseconds) */ | ||
| mtime?: number; /** File content hash (when useCheckSum is enabled) */ | ||
| hash?: string; /** Custom data associated with the file (e.g., lint results, metadata) */ | ||
| data?: unknown; /** Allow any additional custom properties */ | ||
| [key: string]: unknown; | ||
| }; | ||
| type AnalyzedFiles = { | ||
| /** Array of file paths that have changed since last cache */changedFiles: string[]; /** Array of file paths that were not found */ | ||
| notFoundFiles: string[]; /** Array of file paths that have not changed since last cache */ | ||
| notChangedFiles: string[]; | ||
| }; | ||
| /** | ||
| * Create a new FileEntryCache instance from a file path | ||
| * @param filePath - The path to the cache file | ||
| * @param options - create options such as useChecksum, cwd, and more | ||
| * @returns A new FileEntryCache instance | ||
| */ | ||
| declare function createFromFile(filePath: string, options?: CreateOptions): FileEntryCache; | ||
| type CreateOptions = Omit<FileEntryCacheOptions, "cache">; | ||
| /** | ||
| * Create a new FileEntryCache instance | ||
| * @param cacheId - The cache file name | ||
| * @param cacheDirectory - The directory to store the cache file (default: undefined, cache won't be persisted) | ||
| * @param options - Whether to use checksum to detect file changes (default: false) | ||
| * @returns A new FileEntryCache instance | ||
| */ | ||
| declare function create(cacheId: string, cacheDirectory?: string, options?: CreateOptions): FileEntryCache; | ||
| declare class FileEntryDefault { | ||
| static create: typeof create; | ||
| static createFromFile: typeof createFromFile; | ||
| } | ||
| declare class FileEntryCache { | ||
| private _cache; | ||
| private _useCheckSum; | ||
| private _hashAlgorithm; | ||
| private _cwd; | ||
| private _restrictAccessToCwd; | ||
| private _logger?; | ||
| private _useAbsolutePathAsKey; | ||
| private _useModifiedTime; | ||
| /** | ||
| * Snapshot of the persisted meta for each key as of the last load/reconcile. | ||
| * Change detection compares against this baseline (not the working cache) so | ||
| * that repeated `getFileDescriptor()` calls keep reporting a file as changed | ||
| * until the cache is reconciled. The set of keys also tracks which files were | ||
| * visited during the current session so that `reconcile()` only updates those. | ||
| */ | ||
| private _originalMeta; | ||
| /** | ||
| * Create a new FileEntryCache instance | ||
| * @param options - The options for the FileEntryCache (all properties are optional with defaults) | ||
| */ | ||
| constructor(options?: FileEntryCacheOptions); | ||
| /** | ||
| * Get the cache | ||
| * @returns {FlatCache} The cache | ||
| */ | ||
| get cache(): FlatCache; | ||
| /** | ||
| * Set the cache | ||
| * @param {FlatCache} cache - The cache to set | ||
| */ | ||
| set cache(cache: FlatCache); | ||
| /** | ||
| * Get the logger | ||
| * @returns {ILogger | undefined} The logger instance | ||
| */ | ||
| get logger(): ILogger | undefined; | ||
| /** | ||
| * Set the logger | ||
| * @param {ILogger | undefined} logger - The logger to set | ||
| */ | ||
| set logger(logger: ILogger | undefined); | ||
| /** | ||
| * Use the hash to check if the file has changed | ||
| * @returns {boolean} if the hash is used to check if the file has changed (default: false) | ||
| */ | ||
| get useCheckSum(): boolean; | ||
| /** | ||
| * Set the useCheckSum value | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set useCheckSum(value: boolean); | ||
| /** | ||
| * Get the hash algorithm | ||
| * @returns {string} The hash algorithm (default: 'md5') | ||
| */ | ||
| get hashAlgorithm(): string; | ||
| /** | ||
| * Set the hash algorithm | ||
| * @param {string} value - The value to set | ||
| */ | ||
| set hashAlgorithm(value: string); | ||
| /** | ||
| * Get the current working directory | ||
| * @returns {string} The current working directory (default: process.cwd()) | ||
| */ | ||
| get cwd(): string; | ||
| /** | ||
| * Set the current working directory | ||
| * | ||
| * Note: when relative paths are used as cache keys (the default), `cwd` must | ||
| * stay stable across a `getFileDescriptor()` / `reconcile()` cycle. Relative | ||
| * keys are resolved against the *current* `cwd` each time, so changing it | ||
| * mid-run can cause `reconcile()` to resolve a key to a different (missing) | ||
| * path and drop the entry. Use absolute keys (`useAbsolutePathAsKey: true`) | ||
| * if `cwd` must change during a run. | ||
| * @param {string} value - The value to set | ||
| */ | ||
| set cwd(value: string); | ||
| /** | ||
| * Get whether to use modified time for change detection | ||
| * @returns {boolean} Whether modified time (mtime) is used for change detection (default: true) | ||
| */ | ||
| get useModifiedTime(): boolean; | ||
| /** | ||
| * Set whether to use modified time for change detection | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set useModifiedTime(value: boolean); | ||
| /** | ||
| * Get whether to restrict paths to cwd boundaries | ||
| * @returns {boolean} Whether strict path checking is enabled (default: true) | ||
| */ | ||
| get restrictAccessToCwd(): boolean; | ||
| /** | ||
| * Set whether to restrict paths to cwd boundaries | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set restrictAccessToCwd(value: boolean); | ||
| /** | ||
| * Get whether to use absolute path as cache key | ||
| * @returns {boolean} Whether cache keys use absolute paths (default: false) | ||
| */ | ||
| get useAbsolutePathAsKey(): boolean; | ||
| /** | ||
| * Set whether to use absolute path as cache key | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set useAbsolutePathAsKey(value: boolean); | ||
| /** | ||
| * Given a buffer, calculate md5 hash of its content. | ||
| * @method getHash | ||
| * @param {Buffer} buffer buffer to calculate hash on | ||
| * @return {String} content hash digest | ||
| */ | ||
| getHash(buffer: Buffer): string; | ||
| /** | ||
| * Create the key for the file path used for caching. | ||
| * @method createFileKey | ||
| * @param {String} filePath | ||
| * @return {String} | ||
| */ | ||
| createFileKey(filePath: string): string; | ||
| /** | ||
| * Check if the file path is a relative path | ||
| * @method isRelativePath | ||
| * @param filePath - The file path to check | ||
| * @returns {boolean} if the file path is a relative path, false otherwise | ||
| */ | ||
| isRelativePath(filePath: string): boolean; | ||
| /** | ||
| * Delete the cache file from the disk | ||
| * @method deleteCacheFile | ||
| * @return {boolean} true if the file was deleted, false otherwise | ||
| */ | ||
| deleteCacheFile(): boolean; | ||
| /** | ||
| * Remove the cache from the file and clear the memory cache | ||
| * @method destroy | ||
| */ | ||
| destroy(): void; | ||
| /** | ||
| * Remove and Entry From the Cache | ||
| * @method removeEntry | ||
| * @param filePath - The file path to remove from the cache | ||
| */ | ||
| removeEntry(filePath: string): void; | ||
| /** | ||
| * Reconcile the cache | ||
| * @method reconcile | ||
| */ | ||
| reconcile(): void; | ||
| /** | ||
| * Check if the file has changed | ||
| * @method hasFileChanged | ||
| * @param filePath - The file path to check | ||
| * @returns {boolean} if the file has changed, false otherwise | ||
| */ | ||
| hasFileChanged(filePath: string): boolean; | ||
| /** | ||
| * Get the file descriptor for the file path | ||
| * @method getFileDescriptor | ||
| * @param filePath - The file path to get the file descriptor for | ||
| * @param options - The options for getting the file descriptor | ||
| * @returns The file descriptor | ||
| */ | ||
| getFileDescriptor(filePath: string, options?: GetFileDescriptorOptions): FileDescriptor; | ||
| /** | ||
| * Get the file descriptors for the files | ||
| * @method normalizeEntries | ||
| * @param files?: string[] - The files to get the file descriptors for | ||
| * @returns The file descriptors | ||
| */ | ||
| normalizeEntries(files?: string[]): FileDescriptor[]; | ||
| /** | ||
| * Analyze the files | ||
| * @method analyzeFiles | ||
| * @param files - The files to analyze | ||
| * @returns {AnalyzedFiles} The analysis of the files | ||
| */ | ||
| analyzeFiles(files: string[]): AnalyzedFiles; | ||
| /** | ||
| * Get the updated files | ||
| * @method getUpdatedFiles | ||
| * @param files - The files to get the updated files for | ||
| * @returns {string[]} The updated files | ||
| */ | ||
| getUpdatedFiles(files: string[]): string[]; | ||
| /** | ||
| * Get the file descriptors by path prefix | ||
| * @method getFileDescriptorsByPath | ||
| * @param filePath - the path prefix to match | ||
| * @returns {FileDescriptor[]} The file descriptors | ||
| */ | ||
| getFileDescriptorsByPath(filePath: string): FileDescriptor[]; | ||
| /** | ||
| * Get the Absolute Path. If it is already absolute it will return the path as is. | ||
| * When restrictAccessToCwd is enabled, ensures the resolved path stays within cwd boundaries. | ||
| * @method getAbsolutePath | ||
| * @param filePath - The file path to get the absolute path for | ||
| * @returns {string} | ||
| * @throws {Error} When restrictAccessToCwd is true and path would resolve outside cwd | ||
| */ | ||
| getAbsolutePath(filePath: string): string; | ||
| /** | ||
| * Get the Absolute Path with a custom working directory. If it is already absolute it will return the path as is. | ||
| * When restrictAccessToCwd is enabled, ensures the resolved path stays within the provided cwd boundaries. | ||
| * @method getAbsolutePathWithCwd | ||
| * @param filePath - The file path to get the absolute path for | ||
| * @param cwd - The custom working directory to resolve relative paths from | ||
| * @returns {string} | ||
| * @throws {Error} When restrictAccessToCwd is true and path would resolve outside the provided cwd | ||
| */ | ||
| getAbsolutePathWithCwd(filePath: string, cwd: string): string; | ||
| /** | ||
| * Rename cache keys that start with a given path prefix. | ||
| * @method renameCacheKeys | ||
| * @param oldPath - The old path prefix to rename | ||
| * @param newPath - The new path prefix to rename to | ||
| */ | ||
| renameCacheKeys(oldPath: string, newPath: string): void; | ||
| } | ||
| //#endregion | ||
| export { AnalyzedFiles, CreateOptions, FileDescriptor, FileDescriptorMeta, FileEntryCache, FileEntryCacheOptions, GetFileDescriptorOptions, ILogger, create, createFromFile, FileEntryDefault as default }; |
+519
| import crypto from "node:crypto"; | ||
| import fs from "node:fs"; | ||
| import path from "node:path"; | ||
| import { FlatCache, createFromFile as createFromFile$1 } from "flat-cache"; | ||
| //#region src/index.ts | ||
| /** | ||
| * Create a new FileEntryCache instance from a file path | ||
| * @param filePath - The path to the cache file | ||
| * @param options - create options such as useChecksum, cwd, and more | ||
| * @returns A new FileEntryCache instance | ||
| */ | ||
| function createFromFile(filePath, options) { | ||
| return create(path.basename(filePath), path.dirname(filePath), options); | ||
| } | ||
| /** | ||
| * Create a new FileEntryCache instance | ||
| * @param cacheId - The cache file name | ||
| * @param cacheDirectory - The directory to store the cache file (default: undefined, cache won't be persisted) | ||
| * @param options - Whether to use checksum to detect file changes (default: false) | ||
| * @returns A new FileEntryCache instance | ||
| */ | ||
| function create(cacheId, cacheDirectory, options) { | ||
| const opts = { | ||
| ...options, | ||
| cache: { | ||
| cacheId, | ||
| cacheDir: cacheDirectory | ||
| } | ||
| }; | ||
| const fileEntryCache = new FileEntryCache(opts); | ||
| if (cacheDirectory) { | ||
| const cachePath = `${cacheDirectory}/${cacheId}`; | ||
| if (fs.existsSync(cachePath)) try { | ||
| fileEntryCache.cache = createFromFile$1(cachePath, opts.cache); | ||
| } catch (error) { | ||
| if (error instanceof SyntaxError || error instanceof TypeError) fileEntryCache.cache = new FlatCache(opts.cache); | ||
| else throw error; | ||
| } | ||
| } | ||
| return fileEntryCache; | ||
| } | ||
| var FileEntryDefault = class { | ||
| static create = create; | ||
| static createFromFile = createFromFile; | ||
| }; | ||
| var FileEntryCache = class { | ||
| _cache = new FlatCache({ useClone: false }); | ||
| _useCheckSum = false; | ||
| _hashAlgorithm = "md5"; | ||
| _cwd = process.cwd(); | ||
| _restrictAccessToCwd = false; | ||
| _logger; | ||
| _useAbsolutePathAsKey = false; | ||
| _useModifiedTime = true; | ||
| /** | ||
| * Snapshot of the persisted meta for each key as of the last load/reconcile. | ||
| * Change detection compares against this baseline (not the working cache) so | ||
| * that repeated `getFileDescriptor()` calls keep reporting a file as changed | ||
| * until the cache is reconciled. The set of keys also tracks which files were | ||
| * visited during the current session so that `reconcile()` only updates those. | ||
| */ | ||
| _originalMeta = /* @__PURE__ */ new Map(); | ||
| /** | ||
| * Create a new FileEntryCache instance | ||
| * @param options - The options for the FileEntryCache (all properties are optional with defaults) | ||
| */ | ||
| constructor(options) { | ||
| if (options?.cache) this._cache = new FlatCache(options.cache); | ||
| if (options?.useCheckSum) this._useCheckSum = options.useCheckSum; | ||
| if (options?.hashAlgorithm) this._hashAlgorithm = options.hashAlgorithm; | ||
| if (options?.cwd) this._cwd = options.cwd; | ||
| if (options?.useModifiedTime !== void 0) this._useModifiedTime = options.useModifiedTime; | ||
| if (options?.restrictAccessToCwd !== void 0) this._restrictAccessToCwd = options.restrictAccessToCwd; | ||
| if (options?.useAbsolutePathAsKey !== void 0) this._useAbsolutePathAsKey = options.useAbsolutePathAsKey; | ||
| if (options?.logger) this._logger = options.logger; | ||
| } | ||
| /** | ||
| * Get the cache | ||
| * @returns {FlatCache} The cache | ||
| */ | ||
| get cache() { | ||
| return this._cache; | ||
| } | ||
| /** | ||
| * Set the cache | ||
| * @param {FlatCache} cache - The cache to set | ||
| */ | ||
| set cache(cache) { | ||
| this._cache = cache; | ||
| this._originalMeta = /* @__PURE__ */ new Map(); | ||
| } | ||
| /** | ||
| * Get the logger | ||
| * @returns {ILogger | undefined} The logger instance | ||
| */ | ||
| get logger() { | ||
| return this._logger; | ||
| } | ||
| /** | ||
| * Set the logger | ||
| * @param {ILogger | undefined} logger - The logger to set | ||
| */ | ||
| set logger(logger) { | ||
| this._logger = logger; | ||
| } | ||
| /** | ||
| * Use the hash to check if the file has changed | ||
| * @returns {boolean} if the hash is used to check if the file has changed (default: false) | ||
| */ | ||
| get useCheckSum() { | ||
| return this._useCheckSum; | ||
| } | ||
| /** | ||
| * Set the useCheckSum value | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set useCheckSum(value) { | ||
| this._useCheckSum = value; | ||
| } | ||
| /** | ||
| * Get the hash algorithm | ||
| * @returns {string} The hash algorithm (default: 'md5') | ||
| */ | ||
| get hashAlgorithm() { | ||
| return this._hashAlgorithm; | ||
| } | ||
| /** | ||
| * Set the hash algorithm | ||
| * @param {string} value - The value to set | ||
| */ | ||
| set hashAlgorithm(value) { | ||
| this._hashAlgorithm = value; | ||
| } | ||
| /** | ||
| * Get the current working directory | ||
| * @returns {string} The current working directory (default: process.cwd()) | ||
| */ | ||
| get cwd() { | ||
| return this._cwd; | ||
| } | ||
| /** | ||
| * Set the current working directory | ||
| * | ||
| * Note: when relative paths are used as cache keys (the default), `cwd` must | ||
| * stay stable across a `getFileDescriptor()` / `reconcile()` cycle. Relative | ||
| * keys are resolved against the *current* `cwd` each time, so changing it | ||
| * mid-run can cause `reconcile()` to resolve a key to a different (missing) | ||
| * path and drop the entry. Use absolute keys (`useAbsolutePathAsKey: true`) | ||
| * if `cwd` must change during a run. | ||
| * @param {string} value - The value to set | ||
| */ | ||
| set cwd(value) { | ||
| this._cwd = value; | ||
| } | ||
| /** | ||
| * Get whether to use modified time for change detection | ||
| * @returns {boolean} Whether modified time (mtime) is used for change detection (default: true) | ||
| */ | ||
| get useModifiedTime() { | ||
| return this._useModifiedTime; | ||
| } | ||
| /** | ||
| * Set whether to use modified time for change detection | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set useModifiedTime(value) { | ||
| this._useModifiedTime = value; | ||
| } | ||
| /** | ||
| * Get whether to restrict paths to cwd boundaries | ||
| * @returns {boolean} Whether strict path checking is enabled (default: true) | ||
| */ | ||
| get restrictAccessToCwd() { | ||
| return this._restrictAccessToCwd; | ||
| } | ||
| /** | ||
| * Set whether to restrict paths to cwd boundaries | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set restrictAccessToCwd(value) { | ||
| this._restrictAccessToCwd = value; | ||
| } | ||
| /** | ||
| * Get whether to use absolute path as cache key | ||
| * @returns {boolean} Whether cache keys use absolute paths (default: false) | ||
| */ | ||
| get useAbsolutePathAsKey() { | ||
| return this._useAbsolutePathAsKey; | ||
| } | ||
| /** | ||
| * Set whether to use absolute path as cache key | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set useAbsolutePathAsKey(value) { | ||
| this._useAbsolutePathAsKey = value; | ||
| } | ||
| /** | ||
| * Given a buffer, calculate md5 hash of its content. | ||
| * @method getHash | ||
| * @param {Buffer} buffer buffer to calculate hash on | ||
| * @return {String} content hash digest | ||
| */ | ||
| getHash(buffer) { | ||
| return crypto.createHash(this._hashAlgorithm).update(buffer).digest("hex"); | ||
| } | ||
| /** | ||
| * Create the key for the file path used for caching. | ||
| * @method createFileKey | ||
| * @param {String} filePath | ||
| * @return {String} | ||
| */ | ||
| createFileKey(filePath) { | ||
| let result = filePath; | ||
| if (this._useAbsolutePathAsKey && this.isRelativePath(filePath)) result = this.getAbsolutePathWithCwd(filePath, this._cwd); | ||
| return result; | ||
| } | ||
| /** | ||
| * Check if the file path is a relative path | ||
| * @method isRelativePath | ||
| * @param filePath - The file path to check | ||
| * @returns {boolean} if the file path is a relative path, false otherwise | ||
| */ | ||
| isRelativePath(filePath) { | ||
| return !path.isAbsolute(filePath); | ||
| } | ||
| /** | ||
| * Delete the cache file from the disk | ||
| * @method deleteCacheFile | ||
| * @return {boolean} true if the file was deleted, false otherwise | ||
| */ | ||
| deleteCacheFile() { | ||
| return this._cache.removeCacheFile(); | ||
| } | ||
| /** | ||
| * Remove the cache from the file and clear the memory cache | ||
| * @method destroy | ||
| */ | ||
| destroy() { | ||
| this._cache.destroy(); | ||
| this._originalMeta = /* @__PURE__ */ new Map(); | ||
| } | ||
| /** | ||
| * Remove and Entry From the Cache | ||
| * @method removeEntry | ||
| * @param filePath - The file path to remove from the cache | ||
| */ | ||
| removeEntry(filePath) { | ||
| const key = this.createFileKey(filePath); | ||
| this._cache.removeKey(key); | ||
| this._originalMeta.delete(key); | ||
| } | ||
| /** | ||
| * Reconcile the cache | ||
| * @method reconcile | ||
| */ | ||
| reconcile() { | ||
| for (const key of [...this._cache.keys()]) try { | ||
| fs.statSync(this.getAbsolutePath(key)); | ||
| } catch (error) { | ||
| if (error.code === "ENOENT") { | ||
| this._cache.removeKey(key); | ||
| this._originalMeta.delete(key); | ||
| } else this._logger?.error({ | ||
| key, | ||
| error | ||
| }, "reconcile: unable to stat file; keeping cached entry"); | ||
| } | ||
| for (const key of [...this._originalMeta.keys()]) { | ||
| const meta = this._cache.getKey(key); | ||
| if (meta) this._originalMeta.set(key, { ...meta }); | ||
| else this._originalMeta.delete(key); | ||
| } | ||
| this._cache.save(); | ||
| } | ||
| /** | ||
| * Check if the file has changed | ||
| * @method hasFileChanged | ||
| * @param filePath - The file path to check | ||
| * @returns {boolean} if the file has changed, false otherwise | ||
| */ | ||
| hasFileChanged(filePath) { | ||
| let result = false; | ||
| const fileDescriptor = this.getFileDescriptor(filePath); | ||
| if ((!fileDescriptor.err || !fileDescriptor.notFound) && fileDescriptor.changed) result = true; | ||
| return result; | ||
| } | ||
| /** | ||
| * Get the file descriptor for the file path | ||
| * @method getFileDescriptor | ||
| * @param filePath - The file path to get the file descriptor for | ||
| * @param options - The options for getting the file descriptor | ||
| * @returns The file descriptor | ||
| */ | ||
| getFileDescriptor(filePath, options) { | ||
| this._logger?.debug({ | ||
| filePath, | ||
| options | ||
| }, "Getting file descriptor"); | ||
| let fstat; | ||
| const result = { | ||
| key: this.createFileKey(filePath), | ||
| changed: false, | ||
| meta: {} | ||
| }; | ||
| this._logger?.trace({ key: result.key }, "Created file key"); | ||
| const metaCache = this._cache.getKey(result.key); | ||
| if (metaCache) this._logger?.trace({ metaCache }, "Found cached meta"); | ||
| else this._logger?.trace("No cached meta found"); | ||
| result.meta = metaCache ? { ...metaCache } : {}; | ||
| const absolutePath = this.getAbsolutePath(filePath); | ||
| this._logger?.trace({ absolutePath }, "Resolved absolute path"); | ||
| const useCheckSumValue = options?.useCheckSum ?? this._useCheckSum; | ||
| this._logger?.debug({ useCheckSum: useCheckSumValue }, "Using checksum setting"); | ||
| const useModifiedTimeValue = options?.useModifiedTime ?? this.useModifiedTime; | ||
| this._logger?.debug({ useModifiedTime: useModifiedTimeValue }, "Using modified time (mtime) setting"); | ||
| try { | ||
| fstat = fs.statSync(absolutePath); | ||
| result.meta.size = fstat.size; | ||
| result.meta.mtime = fstat.mtime.getTime(); | ||
| this._logger?.trace({ | ||
| size: result.meta.size, | ||
| mtime: result.meta.mtime | ||
| }, "Read file stats"); | ||
| if (useCheckSumValue) { | ||
| const buffer = fs.readFileSync(absolutePath); | ||
| result.meta.hash = this.getHash(buffer); | ||
| this._logger?.trace({ hash: result.meta.hash }, "Calculated file hash"); | ||
| } | ||
| } catch (error) { | ||
| this._logger?.error({ | ||
| filePath, | ||
| error | ||
| }, "Error reading file"); | ||
| this.removeEntry(filePath); | ||
| let notFound = false; | ||
| if (error.message.includes("ENOENT")) { | ||
| notFound = true; | ||
| this._logger?.debug({ filePath }, "File not found"); | ||
| } | ||
| return { | ||
| key: result.key, | ||
| err: error, | ||
| notFound, | ||
| meta: {} | ||
| }; | ||
| } | ||
| if (!this._originalMeta.has(result.key)) this._originalMeta.set(result.key, metaCache ? { ...metaCache } : void 0); | ||
| const baseline = this._originalMeta.get(result.key); | ||
| if (baseline === void 0) { | ||
| result.changed = true; | ||
| this._cache.setKey(result.key, result.meta); | ||
| this._logger?.debug({ filePath }, "File not in cache, marked as changed"); | ||
| return result; | ||
| } | ||
| if (useModifiedTimeValue && baseline.mtime !== result.meta?.mtime) { | ||
| result.changed = true; | ||
| this._logger?.debug({ | ||
| filePath, | ||
| oldMtime: baseline.mtime, | ||
| newMtime: result.meta.mtime | ||
| }, "File changed: mtime differs"); | ||
| } | ||
| if (baseline.size !== result.meta?.size) { | ||
| result.changed = true; | ||
| this._logger?.debug({ | ||
| filePath, | ||
| oldSize: baseline.size, | ||
| newSize: result.meta.size | ||
| }, "File changed: size differs"); | ||
| } | ||
| if (useCheckSumValue && baseline.hash !== result.meta?.hash) { | ||
| result.changed = true; | ||
| this._logger?.debug({ | ||
| filePath, | ||
| oldHash: baseline.hash, | ||
| newHash: result.meta.hash | ||
| }, "File changed: hash differs"); | ||
| } | ||
| this._cache.setKey(result.key, result.meta); | ||
| if (result.changed) this._logger?.info({ filePath }, "File has changed"); | ||
| else this._logger?.debug({ filePath }, "File unchanged"); | ||
| return result; | ||
| } | ||
| /** | ||
| * Get the file descriptors for the files | ||
| * @method normalizeEntries | ||
| * @param files?: string[] - The files to get the file descriptors for | ||
| * @returns The file descriptors | ||
| */ | ||
| normalizeEntries(files) { | ||
| const result = []; | ||
| if (files) { | ||
| for (const file of files) { | ||
| const fileDescriptor = this.getFileDescriptor(file); | ||
| result.push(fileDescriptor); | ||
| } | ||
| return result; | ||
| } | ||
| const keys = this.cache.keys(); | ||
| for (const key of keys) { | ||
| const fileDescriptor = this.getFileDescriptor(key); | ||
| if (!fileDescriptor.notFound && !fileDescriptor.err) result.push(fileDescriptor); | ||
| } | ||
| return result; | ||
| } | ||
| /** | ||
| * Analyze the files | ||
| * @method analyzeFiles | ||
| * @param files - The files to analyze | ||
| * @returns {AnalyzedFiles} The analysis of the files | ||
| */ | ||
| analyzeFiles(files) { | ||
| const result = { | ||
| changedFiles: [], | ||
| notFoundFiles: [], | ||
| notChangedFiles: [] | ||
| }; | ||
| const fileDescriptors = this.normalizeEntries(files); | ||
| for (const fileDescriptor of fileDescriptors) if (fileDescriptor.notFound) result.notFoundFiles.push(fileDescriptor.key); | ||
| else if (fileDescriptor.changed) result.changedFiles.push(fileDescriptor.key); | ||
| else result.notChangedFiles.push(fileDescriptor.key); | ||
| return result; | ||
| } | ||
| /** | ||
| * Get the updated files | ||
| * @method getUpdatedFiles | ||
| * @param files - The files to get the updated files for | ||
| * @returns {string[]} The updated files | ||
| */ | ||
| getUpdatedFiles(files) { | ||
| const result = []; | ||
| const fileDescriptors = this.normalizeEntries(files); | ||
| for (const fileDescriptor of fileDescriptors) if (fileDescriptor.changed) result.push(fileDescriptor.key); | ||
| return result; | ||
| } | ||
| /** | ||
| * Get the file descriptors by path prefix | ||
| * @method getFileDescriptorsByPath | ||
| * @param filePath - the path prefix to match | ||
| * @returns {FileDescriptor[]} The file descriptors | ||
| */ | ||
| getFileDescriptorsByPath(filePath) { | ||
| const result = []; | ||
| const keys = this._cache.keys(); | ||
| for (const key of keys) | ||
| /* v8 ignore next -- @preserve */ | ||
| if (key.startsWith(filePath)) { | ||
| const fileDescriptor = this.getFileDescriptor(key); | ||
| result.push(fileDescriptor); | ||
| } | ||
| return result; | ||
| } | ||
| /** | ||
| * Get the Absolute Path. If it is already absolute it will return the path as is. | ||
| * When restrictAccessToCwd is enabled, ensures the resolved path stays within cwd boundaries. | ||
| * @method getAbsolutePath | ||
| * @param filePath - The file path to get the absolute path for | ||
| * @returns {string} | ||
| * @throws {Error} When restrictAccessToCwd is true and path would resolve outside cwd | ||
| */ | ||
| getAbsolutePath(filePath) { | ||
| if (this.isRelativePath(filePath)) { | ||
| const sanitizedPath = filePath.replace(/\0/g, ""); | ||
| const resolved = path.resolve(this._cwd, sanitizedPath); | ||
| if (this._restrictAccessToCwd) { | ||
| const normalizedResolved = path.normalize(resolved); | ||
| const normalizedCwd = path.normalize(this._cwd); | ||
| if (!(normalizedResolved === normalizedCwd || normalizedResolved.startsWith(normalizedCwd + path.sep))) throw new Error(`Path traversal attempt blocked: "${filePath}" resolves outside of working directory "${this._cwd}"`); | ||
| } | ||
| return resolved; | ||
| } | ||
| return filePath; | ||
| } | ||
| /** | ||
| * Get the Absolute Path with a custom working directory. If it is already absolute it will return the path as is. | ||
| * When restrictAccessToCwd is enabled, ensures the resolved path stays within the provided cwd boundaries. | ||
| * @method getAbsolutePathWithCwd | ||
| * @param filePath - The file path to get the absolute path for | ||
| * @param cwd - The custom working directory to resolve relative paths from | ||
| * @returns {string} | ||
| * @throws {Error} When restrictAccessToCwd is true and path would resolve outside the provided cwd | ||
| */ | ||
| getAbsolutePathWithCwd(filePath, cwd) { | ||
| if (this.isRelativePath(filePath)) { | ||
| const sanitizedPath = filePath.replace(/\0/g, ""); | ||
| const resolved = path.resolve(cwd, sanitizedPath); | ||
| if (this._restrictAccessToCwd) { | ||
| const normalizedResolved = path.normalize(resolved); | ||
| const normalizedCwd = path.normalize(cwd); | ||
| if (!(normalizedResolved === normalizedCwd || normalizedResolved.startsWith(normalizedCwd + path.sep))) throw new Error(`Path traversal attempt blocked: "${filePath}" resolves outside of working directory "${cwd}"`); | ||
| } | ||
| return resolved; | ||
| } | ||
| return filePath; | ||
| } | ||
| /** | ||
| * Rename cache keys that start with a given path prefix. | ||
| * @method renameCacheKeys | ||
| * @param oldPath - The old path prefix to rename | ||
| * @param newPath - The new path prefix to rename to | ||
| */ | ||
| renameCacheKeys(oldPath, newPath) { | ||
| const keys = this._cache.keys(); | ||
| for (const key of keys) | ||
| /* v8 ignore next -- @preserve */ | ||
| if (key.startsWith(oldPath)) { | ||
| const newKey = key.replace(oldPath, newPath); | ||
| const meta = this._cache.getKey(key); | ||
| this._cache.removeKey(key); | ||
| this._cache.setKey(newKey, meta); | ||
| if (this._originalMeta.has(key)) { | ||
| this._originalMeta.set(newKey, this._originalMeta.get(key)); | ||
| this._originalMeta.delete(key); | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| //#endregion | ||
| export { FileEntryCache, create, createFromFile, FileEntryDefault as default }; |
+535
-543
@@ -1,2 +0,6 @@ | ||
| "use strict"; | ||
| Object.defineProperties(exports, { | ||
| __esModule: { value: true }, | ||
| [Symbol.toStringTag]: { value: "Module" } | ||
| }); | ||
| //#region \0rolldown/runtime.js | ||
| var __create = Object.create; | ||
@@ -8,553 +12,541 @@ var __defProp = Object.defineProperty; | ||
| var __hasOwnProp = Object.prototype.hasOwnProperty; | ||
| var __export = (target, all) => { | ||
| for (var name in all) | ||
| __defProp(target, name, { get: all[name], enumerable: true }); | ||
| }; | ||
| var __copyProps = (to, from, except, desc) => { | ||
| if (from && typeof from === "object" || typeof from === "function") { | ||
| for (let key of __getOwnPropNames(from)) | ||
| if (!__hasOwnProp.call(to, key) && key !== except) | ||
| __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); | ||
| } | ||
| return to; | ||
| if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { | ||
| key = keys[i]; | ||
| if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { | ||
| get: ((k) => from[k]).bind(null, key), | ||
| enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable | ||
| }); | ||
| } | ||
| return to; | ||
| }; | ||
| var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( | ||
| // If the importer is in node compatibility mode or this is not an ESM | ||
| // file that has been converted to a CommonJS file using a Babel- | ||
| // compatible transform (i.e. "__esModule" has not been set), then set | ||
| // "default" to the CommonJS "module.exports" for node compatibility. | ||
| isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, | ||
| mod | ||
| )); | ||
| var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); | ||
| // src/index.ts | ||
| var index_exports = {}; | ||
| __export(index_exports, { | ||
| FileEntryCache: () => FileEntryCache, | ||
| create: () => create, | ||
| createFromFile: () => createFromFile, | ||
| default: () => FileEntryDefault | ||
| }); | ||
| module.exports = __toCommonJS(index_exports); | ||
| var import_node_crypto = __toESM(require("crypto"), 1); | ||
| var import_node_fs = __toESM(require("fs"), 1); | ||
| var import_node_path = __toESM(require("path"), 1); | ||
| var import_flat_cache = require("flat-cache"); | ||
| var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { | ||
| value: mod, | ||
| enumerable: true | ||
| }) : target, mod)); | ||
| //#endregion | ||
| let node_crypto = require("node:crypto"); | ||
| node_crypto = __toESM(node_crypto, 1); | ||
| let node_fs = require("node:fs"); | ||
| node_fs = __toESM(node_fs, 1); | ||
| let node_path = require("node:path"); | ||
| node_path = __toESM(node_path, 1); | ||
| let flat_cache = require("flat-cache"); | ||
| //#region src/index.ts | ||
| /** | ||
| * Create a new FileEntryCache instance from a file path | ||
| * @param filePath - The path to the cache file | ||
| * @param options - create options such as useChecksum, cwd, and more | ||
| * @returns A new FileEntryCache instance | ||
| */ | ||
| function createFromFile(filePath, options) { | ||
| const fname = import_node_path.default.basename(filePath); | ||
| const directory = import_node_path.default.dirname(filePath); | ||
| return create(fname, directory, options); | ||
| return create(node_path.default.basename(filePath), node_path.default.dirname(filePath), options); | ||
| } | ||
| /** | ||
| * Create a new FileEntryCache instance | ||
| * @param cacheId - The cache file name | ||
| * @param cacheDirectory - The directory to store the cache file (default: undefined, cache won't be persisted) | ||
| * @param options - Whether to use checksum to detect file changes (default: false) | ||
| * @returns A new FileEntryCache instance | ||
| */ | ||
| function create(cacheId, cacheDirectory, options) { | ||
| const opts = { | ||
| ...options, | ||
| cache: { | ||
| cacheId, | ||
| cacheDir: cacheDirectory | ||
| } | ||
| }; | ||
| const fileEntryCache = new FileEntryCache(opts); | ||
| if (cacheDirectory) { | ||
| const cachePath = `${cacheDirectory}/${cacheId}`; | ||
| if (import_node_fs.default.existsSync(cachePath)) { | ||
| fileEntryCache.cache = (0, import_flat_cache.createFromFile)(cachePath, opts.cache); | ||
| } | ||
| } | ||
| return fileEntryCache; | ||
| const opts = { | ||
| ...options, | ||
| cache: { | ||
| cacheId, | ||
| cacheDir: cacheDirectory | ||
| } | ||
| }; | ||
| const fileEntryCache = new FileEntryCache(opts); | ||
| if (cacheDirectory) { | ||
| const cachePath = `${cacheDirectory}/${cacheId}`; | ||
| if (node_fs.default.existsSync(cachePath)) try { | ||
| fileEntryCache.cache = (0, flat_cache.createFromFile)(cachePath, opts.cache); | ||
| } catch (error) { | ||
| if (error instanceof SyntaxError || error instanceof TypeError) fileEntryCache.cache = new flat_cache.FlatCache(opts.cache); | ||
| else throw error; | ||
| } | ||
| } | ||
| return fileEntryCache; | ||
| } | ||
| var FileEntryDefault = class { | ||
| static create = create; | ||
| static createFromFile = createFromFile; | ||
| static create = create; | ||
| static createFromFile = createFromFile; | ||
| }; | ||
| var FileEntryCache = class { | ||
| _cache = new import_flat_cache.FlatCache({ useClone: false }); | ||
| _useCheckSum = false; | ||
| _hashAlgorithm = "md5"; | ||
| _cwd = process.cwd(); | ||
| _restrictAccessToCwd = false; | ||
| _logger; | ||
| _useAbsolutePathAsKey = false; | ||
| _useModifiedTime = true; | ||
| /** | ||
| * Create a new FileEntryCache instance | ||
| * @param options - The options for the FileEntryCache (all properties are optional with defaults) | ||
| */ | ||
| constructor(options) { | ||
| if (options?.cache) { | ||
| this._cache = new import_flat_cache.FlatCache(options.cache); | ||
| } | ||
| if (options?.useCheckSum) { | ||
| this._useCheckSum = options.useCheckSum; | ||
| } | ||
| if (options?.hashAlgorithm) { | ||
| this._hashAlgorithm = options.hashAlgorithm; | ||
| } | ||
| if (options?.cwd) { | ||
| this._cwd = options.cwd; | ||
| } | ||
| if (options?.useModifiedTime !== void 0) { | ||
| this._useModifiedTime = options.useModifiedTime; | ||
| } | ||
| if (options?.restrictAccessToCwd !== void 0) { | ||
| this._restrictAccessToCwd = options.restrictAccessToCwd; | ||
| } | ||
| if (options?.useAbsolutePathAsKey !== void 0) { | ||
| this._useAbsolutePathAsKey = options.useAbsolutePathAsKey; | ||
| } | ||
| if (options?.logger) { | ||
| this._logger = options.logger; | ||
| } | ||
| } | ||
| /** | ||
| * Get the cache | ||
| * @returns {FlatCache} The cache | ||
| */ | ||
| get cache() { | ||
| return this._cache; | ||
| } | ||
| /** | ||
| * Set the cache | ||
| * @param {FlatCache} cache - The cache to set | ||
| */ | ||
| set cache(cache) { | ||
| this._cache = cache; | ||
| } | ||
| /** | ||
| * Get the logger | ||
| * @returns {ILogger | undefined} The logger instance | ||
| */ | ||
| get logger() { | ||
| return this._logger; | ||
| } | ||
| /** | ||
| * Set the logger | ||
| * @param {ILogger | undefined} logger - The logger to set | ||
| */ | ||
| set logger(logger) { | ||
| this._logger = logger; | ||
| } | ||
| /** | ||
| * Use the hash to check if the file has changed | ||
| * @returns {boolean} if the hash is used to check if the file has changed (default: false) | ||
| */ | ||
| get useCheckSum() { | ||
| return this._useCheckSum; | ||
| } | ||
| /** | ||
| * Set the useCheckSum value | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set useCheckSum(value) { | ||
| this._useCheckSum = value; | ||
| } | ||
| /** | ||
| * Get the hash algorithm | ||
| * @returns {string} The hash algorithm (default: 'md5') | ||
| */ | ||
| get hashAlgorithm() { | ||
| return this._hashAlgorithm; | ||
| } | ||
| /** | ||
| * Set the hash algorithm | ||
| * @param {string} value - The value to set | ||
| */ | ||
| set hashAlgorithm(value) { | ||
| this._hashAlgorithm = value; | ||
| } | ||
| /** | ||
| * Get the current working directory | ||
| * @returns {string} The current working directory (default: process.cwd()) | ||
| */ | ||
| get cwd() { | ||
| return this._cwd; | ||
| } | ||
| /** | ||
| * Set the current working directory | ||
| * @param {string} value - The value to set | ||
| */ | ||
| set cwd(value) { | ||
| this._cwd = value; | ||
| } | ||
| /** | ||
| * Get whether to use modified time for change detection | ||
| * @returns {boolean} Whether modified time (mtime) is used for change detection (default: true) | ||
| */ | ||
| get useModifiedTime() { | ||
| return this._useModifiedTime; | ||
| } | ||
| /** | ||
| * Set whether to use modified time for change detection | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set useModifiedTime(value) { | ||
| this._useModifiedTime = value; | ||
| } | ||
| /** | ||
| * Get whether to restrict paths to cwd boundaries | ||
| * @returns {boolean} Whether strict path checking is enabled (default: true) | ||
| */ | ||
| get restrictAccessToCwd() { | ||
| return this._restrictAccessToCwd; | ||
| } | ||
| /** | ||
| * Set whether to restrict paths to cwd boundaries | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set restrictAccessToCwd(value) { | ||
| this._restrictAccessToCwd = value; | ||
| } | ||
| /** | ||
| * Get whether to use absolute path as cache key | ||
| * @returns {boolean} Whether cache keys use absolute paths (default: false) | ||
| */ | ||
| get useAbsolutePathAsKey() { | ||
| return this._useAbsolutePathAsKey; | ||
| } | ||
| /** | ||
| * Set whether to use absolute path as cache key | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set useAbsolutePathAsKey(value) { | ||
| this._useAbsolutePathAsKey = value; | ||
| } | ||
| /** | ||
| * Given a buffer, calculate md5 hash of its content. | ||
| * @method getHash | ||
| * @param {Buffer} buffer buffer to calculate hash on | ||
| * @return {String} content hash digest | ||
| */ | ||
| getHash(buffer) { | ||
| return import_node_crypto.default.createHash(this._hashAlgorithm).update(buffer).digest("hex"); | ||
| } | ||
| /** | ||
| * Create the key for the file path used for caching. | ||
| * @method createFileKey | ||
| * @param {String} filePath | ||
| * @return {String} | ||
| */ | ||
| createFileKey(filePath) { | ||
| let result = filePath; | ||
| if (this._useAbsolutePathAsKey && this.isRelativePath(filePath)) { | ||
| result = this.getAbsolutePathWithCwd(filePath, this._cwd); | ||
| } | ||
| return result; | ||
| } | ||
| /** | ||
| * Check if the file path is a relative path | ||
| * @method isRelativePath | ||
| * @param filePath - The file path to check | ||
| * @returns {boolean} if the file path is a relative path, false otherwise | ||
| */ | ||
| isRelativePath(filePath) { | ||
| return !import_node_path.default.isAbsolute(filePath); | ||
| } | ||
| /** | ||
| * Delete the cache file from the disk | ||
| * @method deleteCacheFile | ||
| * @return {boolean} true if the file was deleted, false otherwise | ||
| */ | ||
| deleteCacheFile() { | ||
| return this._cache.removeCacheFile(); | ||
| } | ||
| /** | ||
| * Remove the cache from the file and clear the memory cache | ||
| * @method destroy | ||
| */ | ||
| destroy() { | ||
| this._cache.destroy(); | ||
| } | ||
| /** | ||
| * Remove and Entry From the Cache | ||
| * @method removeEntry | ||
| * @param filePath - The file path to remove from the cache | ||
| */ | ||
| removeEntry(filePath) { | ||
| const key = this.createFileKey(filePath); | ||
| this._cache.removeKey(key); | ||
| } | ||
| /** | ||
| * Reconcile the cache | ||
| * @method reconcile | ||
| */ | ||
| reconcile() { | ||
| const { items } = this._cache; | ||
| for (const item of items) { | ||
| const fileDescriptor = this.getFileDescriptor(item.key); | ||
| if (fileDescriptor.notFound) { | ||
| this._cache.removeKey(item.key); | ||
| } | ||
| } | ||
| this._cache.save(); | ||
| } | ||
| /** | ||
| * Check if the file has changed | ||
| * @method hasFileChanged | ||
| * @param filePath - The file path to check | ||
| * @returns {boolean} if the file has changed, false otherwise | ||
| */ | ||
| hasFileChanged(filePath) { | ||
| let result = false; | ||
| const fileDescriptor = this.getFileDescriptor(filePath); | ||
| if ((!fileDescriptor.err || !fileDescriptor.notFound) && fileDescriptor.changed) { | ||
| result = true; | ||
| } | ||
| return result; | ||
| } | ||
| /** | ||
| * Get the file descriptor for the file path | ||
| * @method getFileDescriptor | ||
| * @param filePath - The file path to get the file descriptor for | ||
| * @param options - The options for getting the file descriptor | ||
| * @returns The file descriptor | ||
| */ | ||
| getFileDescriptor(filePath, options) { | ||
| this._logger?.debug({ filePath, options }, "Getting file descriptor"); | ||
| let fstat; | ||
| const result = { | ||
| key: this.createFileKey(filePath), | ||
| changed: false, | ||
| meta: {} | ||
| }; | ||
| this._logger?.trace({ key: result.key }, "Created file key"); | ||
| const metaCache = this._cache.getKey(result.key); | ||
| if (metaCache) { | ||
| this._logger?.trace({ metaCache }, "Found cached meta"); | ||
| } else { | ||
| this._logger?.trace("No cached meta found"); | ||
| } | ||
| result.meta = metaCache ? { ...metaCache } : {}; | ||
| const absolutePath = this.getAbsolutePath(filePath); | ||
| this._logger?.trace({ absolutePath }, "Resolved absolute path"); | ||
| const useCheckSumValue = options?.useCheckSum ?? this._useCheckSum; | ||
| this._logger?.debug( | ||
| { useCheckSum: useCheckSumValue }, | ||
| "Using checksum setting" | ||
| ); | ||
| const useModifiedTimeValue = options?.useModifiedTime ?? this.useModifiedTime; | ||
| this._logger?.debug( | ||
| { useModifiedTime: useModifiedTimeValue }, | ||
| "Using modified time (mtime) setting" | ||
| ); | ||
| try { | ||
| fstat = import_node_fs.default.statSync(absolutePath); | ||
| result.meta.size = fstat.size; | ||
| result.meta.mtime = fstat.mtime.getTime(); | ||
| this._logger?.trace( | ||
| { size: result.meta.size, mtime: result.meta.mtime }, | ||
| "Read file stats" | ||
| ); | ||
| if (useCheckSumValue) { | ||
| const buffer = import_node_fs.default.readFileSync(absolutePath); | ||
| result.meta.hash = this.getHash(buffer); | ||
| this._logger?.trace({ hash: result.meta.hash }, "Calculated file hash"); | ||
| } | ||
| } catch (error) { | ||
| this._logger?.error({ filePath, error }, "Error reading file"); | ||
| this.removeEntry(filePath); | ||
| let notFound = false; | ||
| if (error.message.includes("ENOENT")) { | ||
| notFound = true; | ||
| this._logger?.debug({ filePath }, "File not found"); | ||
| } | ||
| return { | ||
| key: result.key, | ||
| err: error, | ||
| notFound, | ||
| meta: {} | ||
| }; | ||
| } | ||
| if (!metaCache) { | ||
| result.changed = true; | ||
| this._cache.setKey(result.key, result.meta); | ||
| this._logger?.debug({ filePath }, "File not in cache, marked as changed"); | ||
| return result; | ||
| } | ||
| if (useModifiedTimeValue && metaCache?.mtime !== result.meta?.mtime) { | ||
| result.changed = true; | ||
| this._logger?.debug( | ||
| { filePath, oldMtime: metaCache.mtime, newMtime: result.meta.mtime }, | ||
| "File changed: mtime differs" | ||
| ); | ||
| } | ||
| if (metaCache?.size !== result.meta?.size) { | ||
| result.changed = true; | ||
| this._logger?.debug( | ||
| { filePath, oldSize: metaCache.size, newSize: result.meta.size }, | ||
| "File changed: size differs" | ||
| ); | ||
| } | ||
| if (useCheckSumValue && metaCache?.hash !== result.meta?.hash) { | ||
| result.changed = true; | ||
| this._logger?.debug( | ||
| { filePath, oldHash: metaCache.hash, newHash: result.meta.hash }, | ||
| "File changed: hash differs" | ||
| ); | ||
| } | ||
| this._cache.setKey(result.key, result.meta); | ||
| if (result.changed) { | ||
| this._logger?.info({ filePath }, "File has changed"); | ||
| } else { | ||
| this._logger?.debug({ filePath }, "File unchanged"); | ||
| } | ||
| return result; | ||
| } | ||
| /** | ||
| * Get the file descriptors for the files | ||
| * @method normalizeEntries | ||
| * @param files?: string[] - The files to get the file descriptors for | ||
| * @returns The file descriptors | ||
| */ | ||
| normalizeEntries(files) { | ||
| const result = []; | ||
| if (files) { | ||
| for (const file of files) { | ||
| const fileDescriptor = this.getFileDescriptor(file); | ||
| result.push(fileDescriptor); | ||
| } | ||
| return result; | ||
| } | ||
| const keys = this.cache.keys(); | ||
| for (const key of keys) { | ||
| const fileDescriptor = this.getFileDescriptor(key); | ||
| if (!fileDescriptor.notFound && !fileDescriptor.err) { | ||
| result.push(fileDescriptor); | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| /** | ||
| * Analyze the files | ||
| * @method analyzeFiles | ||
| * @param files - The files to analyze | ||
| * @returns {AnalyzedFiles} The analysis of the files | ||
| */ | ||
| analyzeFiles(files) { | ||
| const result = { | ||
| changedFiles: [], | ||
| notFoundFiles: [], | ||
| notChangedFiles: [] | ||
| }; | ||
| const fileDescriptors = this.normalizeEntries(files); | ||
| for (const fileDescriptor of fileDescriptors) { | ||
| if (fileDescriptor.notFound) { | ||
| result.notFoundFiles.push(fileDescriptor.key); | ||
| } else if (fileDescriptor.changed) { | ||
| result.changedFiles.push(fileDescriptor.key); | ||
| } else { | ||
| result.notChangedFiles.push(fileDescriptor.key); | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| /** | ||
| * Get the updated files | ||
| * @method getUpdatedFiles | ||
| * @param files - The files to get the updated files for | ||
| * @returns {string[]} The updated files | ||
| */ | ||
| getUpdatedFiles(files) { | ||
| const result = []; | ||
| const fileDescriptors = this.normalizeEntries(files); | ||
| for (const fileDescriptor of fileDescriptors) { | ||
| if (fileDescriptor.changed) { | ||
| result.push(fileDescriptor.key); | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| /** | ||
| * Get the file descriptors by path prefix | ||
| * @method getFileDescriptorsByPath | ||
| * @param filePath - the path prefix to match | ||
| * @returns {FileDescriptor[]} The file descriptors | ||
| */ | ||
| getFileDescriptorsByPath(filePath) { | ||
| const result = []; | ||
| const keys = this._cache.keys(); | ||
| for (const key of keys) { | ||
| if (key.startsWith(filePath)) { | ||
| const fileDescriptor = this.getFileDescriptor(key); | ||
| result.push(fileDescriptor); | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| /** | ||
| * Get the Absolute Path. If it is already absolute it will return the path as is. | ||
| * When restrictAccessToCwd is enabled, ensures the resolved path stays within cwd boundaries. | ||
| * @method getAbsolutePath | ||
| * @param filePath - The file path to get the absolute path for | ||
| * @returns {string} | ||
| * @throws {Error} When restrictAccessToCwd is true and path would resolve outside cwd | ||
| */ | ||
| getAbsolutePath(filePath) { | ||
| if (this.isRelativePath(filePath)) { | ||
| const sanitizedPath = filePath.replace(/\0/g, ""); | ||
| const resolved = import_node_path.default.resolve(this._cwd, sanitizedPath); | ||
| if (this._restrictAccessToCwd) { | ||
| const normalizedResolved = import_node_path.default.normalize(resolved); | ||
| const normalizedCwd = import_node_path.default.normalize(this._cwd); | ||
| const isWithinCwd = normalizedResolved === normalizedCwd || normalizedResolved.startsWith(normalizedCwd + import_node_path.default.sep); | ||
| if (!isWithinCwd) { | ||
| throw new Error( | ||
| `Path traversal attempt blocked: "${filePath}" resolves outside of working directory "${this._cwd}"` | ||
| ); | ||
| } | ||
| } | ||
| return resolved; | ||
| } | ||
| return filePath; | ||
| } | ||
| /** | ||
| * Get the Absolute Path with a custom working directory. If it is already absolute it will return the path as is. | ||
| * When restrictAccessToCwd is enabled, ensures the resolved path stays within the provided cwd boundaries. | ||
| * @method getAbsolutePathWithCwd | ||
| * @param filePath - The file path to get the absolute path for | ||
| * @param cwd - The custom working directory to resolve relative paths from | ||
| * @returns {string} | ||
| * @throws {Error} When restrictAccessToCwd is true and path would resolve outside the provided cwd | ||
| */ | ||
| getAbsolutePathWithCwd(filePath, cwd) { | ||
| if (this.isRelativePath(filePath)) { | ||
| const sanitizedPath = filePath.replace(/\0/g, ""); | ||
| const resolved = import_node_path.default.resolve(cwd, sanitizedPath); | ||
| if (this._restrictAccessToCwd) { | ||
| const normalizedResolved = import_node_path.default.normalize(resolved); | ||
| const normalizedCwd = import_node_path.default.normalize(cwd); | ||
| const isWithinCwd = normalizedResolved === normalizedCwd || normalizedResolved.startsWith(normalizedCwd + import_node_path.default.sep); | ||
| if (!isWithinCwd) { | ||
| throw new Error( | ||
| `Path traversal attempt blocked: "${filePath}" resolves outside of working directory "${cwd}"` | ||
| ); | ||
| } | ||
| } | ||
| return resolved; | ||
| } | ||
| return filePath; | ||
| } | ||
| /** | ||
| * Rename cache keys that start with a given path prefix. | ||
| * @method renameCacheKeys | ||
| * @param oldPath - The old path prefix to rename | ||
| * @param newPath - The new path prefix to rename to | ||
| */ | ||
| renameCacheKeys(oldPath, newPath) { | ||
| const keys = this._cache.keys(); | ||
| for (const key of keys) { | ||
| if (key.startsWith(oldPath)) { | ||
| const newKey = key.replace(oldPath, newPath); | ||
| const meta = this._cache.getKey(key); | ||
| this._cache.removeKey(key); | ||
| this._cache.setKey(newKey, meta); | ||
| } | ||
| } | ||
| } | ||
| _cache = new flat_cache.FlatCache({ useClone: false }); | ||
| _useCheckSum = false; | ||
| _hashAlgorithm = "md5"; | ||
| _cwd = process.cwd(); | ||
| _restrictAccessToCwd = false; | ||
| _logger; | ||
| _useAbsolutePathAsKey = false; | ||
| _useModifiedTime = true; | ||
| /** | ||
| * Snapshot of the persisted meta for each key as of the last load/reconcile. | ||
| * Change detection compares against this baseline (not the working cache) so | ||
| * that repeated `getFileDescriptor()` calls keep reporting a file as changed | ||
| * until the cache is reconciled. The set of keys also tracks which files were | ||
| * visited during the current session so that `reconcile()` only updates those. | ||
| */ | ||
| _originalMeta = /* @__PURE__ */ new Map(); | ||
| /** | ||
| * Create a new FileEntryCache instance | ||
| * @param options - The options for the FileEntryCache (all properties are optional with defaults) | ||
| */ | ||
| constructor(options) { | ||
| if (options?.cache) this._cache = new flat_cache.FlatCache(options.cache); | ||
| if (options?.useCheckSum) this._useCheckSum = options.useCheckSum; | ||
| if (options?.hashAlgorithm) this._hashAlgorithm = options.hashAlgorithm; | ||
| if (options?.cwd) this._cwd = options.cwd; | ||
| if (options?.useModifiedTime !== void 0) this._useModifiedTime = options.useModifiedTime; | ||
| if (options?.restrictAccessToCwd !== void 0) this._restrictAccessToCwd = options.restrictAccessToCwd; | ||
| if (options?.useAbsolutePathAsKey !== void 0) this._useAbsolutePathAsKey = options.useAbsolutePathAsKey; | ||
| if (options?.logger) this._logger = options.logger; | ||
| } | ||
| /** | ||
| * Get the cache | ||
| * @returns {FlatCache} The cache | ||
| */ | ||
| get cache() { | ||
| return this._cache; | ||
| } | ||
| /** | ||
| * Set the cache | ||
| * @param {FlatCache} cache - The cache to set | ||
| */ | ||
| set cache(cache) { | ||
| this._cache = cache; | ||
| this._originalMeta = /* @__PURE__ */ new Map(); | ||
| } | ||
| /** | ||
| * Get the logger | ||
| * @returns {ILogger | undefined} The logger instance | ||
| */ | ||
| get logger() { | ||
| return this._logger; | ||
| } | ||
| /** | ||
| * Set the logger | ||
| * @param {ILogger | undefined} logger - The logger to set | ||
| */ | ||
| set logger(logger) { | ||
| this._logger = logger; | ||
| } | ||
| /** | ||
| * Use the hash to check if the file has changed | ||
| * @returns {boolean} if the hash is used to check if the file has changed (default: false) | ||
| */ | ||
| get useCheckSum() { | ||
| return this._useCheckSum; | ||
| } | ||
| /** | ||
| * Set the useCheckSum value | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set useCheckSum(value) { | ||
| this._useCheckSum = value; | ||
| } | ||
| /** | ||
| * Get the hash algorithm | ||
| * @returns {string} The hash algorithm (default: 'md5') | ||
| */ | ||
| get hashAlgorithm() { | ||
| return this._hashAlgorithm; | ||
| } | ||
| /** | ||
| * Set the hash algorithm | ||
| * @param {string} value - The value to set | ||
| */ | ||
| set hashAlgorithm(value) { | ||
| this._hashAlgorithm = value; | ||
| } | ||
| /** | ||
| * Get the current working directory | ||
| * @returns {string} The current working directory (default: process.cwd()) | ||
| */ | ||
| get cwd() { | ||
| return this._cwd; | ||
| } | ||
| /** | ||
| * Set the current working directory | ||
| * | ||
| * Note: when relative paths are used as cache keys (the default), `cwd` must | ||
| * stay stable across a `getFileDescriptor()` / `reconcile()` cycle. Relative | ||
| * keys are resolved against the *current* `cwd` each time, so changing it | ||
| * mid-run can cause `reconcile()` to resolve a key to a different (missing) | ||
| * path and drop the entry. Use absolute keys (`useAbsolutePathAsKey: true`) | ||
| * if `cwd` must change during a run. | ||
| * @param {string} value - The value to set | ||
| */ | ||
| set cwd(value) { | ||
| this._cwd = value; | ||
| } | ||
| /** | ||
| * Get whether to use modified time for change detection | ||
| * @returns {boolean} Whether modified time (mtime) is used for change detection (default: true) | ||
| */ | ||
| get useModifiedTime() { | ||
| return this._useModifiedTime; | ||
| } | ||
| /** | ||
| * Set whether to use modified time for change detection | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set useModifiedTime(value) { | ||
| this._useModifiedTime = value; | ||
| } | ||
| /** | ||
| * Get whether to restrict paths to cwd boundaries | ||
| * @returns {boolean} Whether strict path checking is enabled (default: true) | ||
| */ | ||
| get restrictAccessToCwd() { | ||
| return this._restrictAccessToCwd; | ||
| } | ||
| /** | ||
| * Set whether to restrict paths to cwd boundaries | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set restrictAccessToCwd(value) { | ||
| this._restrictAccessToCwd = value; | ||
| } | ||
| /** | ||
| * Get whether to use absolute path as cache key | ||
| * @returns {boolean} Whether cache keys use absolute paths (default: false) | ||
| */ | ||
| get useAbsolutePathAsKey() { | ||
| return this._useAbsolutePathAsKey; | ||
| } | ||
| /** | ||
| * Set whether to use absolute path as cache key | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set useAbsolutePathAsKey(value) { | ||
| this._useAbsolutePathAsKey = value; | ||
| } | ||
| /** | ||
| * Given a buffer, calculate md5 hash of its content. | ||
| * @method getHash | ||
| * @param {Buffer} buffer buffer to calculate hash on | ||
| * @return {String} content hash digest | ||
| */ | ||
| getHash(buffer) { | ||
| return node_crypto.default.createHash(this._hashAlgorithm).update(buffer).digest("hex"); | ||
| } | ||
| /** | ||
| * Create the key for the file path used for caching. | ||
| * @method createFileKey | ||
| * @param {String} filePath | ||
| * @return {String} | ||
| */ | ||
| createFileKey(filePath) { | ||
| let result = filePath; | ||
| if (this._useAbsolutePathAsKey && this.isRelativePath(filePath)) result = this.getAbsolutePathWithCwd(filePath, this._cwd); | ||
| return result; | ||
| } | ||
| /** | ||
| * Check if the file path is a relative path | ||
| * @method isRelativePath | ||
| * @param filePath - The file path to check | ||
| * @returns {boolean} if the file path is a relative path, false otherwise | ||
| */ | ||
| isRelativePath(filePath) { | ||
| return !node_path.default.isAbsolute(filePath); | ||
| } | ||
| /** | ||
| * Delete the cache file from the disk | ||
| * @method deleteCacheFile | ||
| * @return {boolean} true if the file was deleted, false otherwise | ||
| */ | ||
| deleteCacheFile() { | ||
| return this._cache.removeCacheFile(); | ||
| } | ||
| /** | ||
| * Remove the cache from the file and clear the memory cache | ||
| * @method destroy | ||
| */ | ||
| destroy() { | ||
| this._cache.destroy(); | ||
| this._originalMeta = /* @__PURE__ */ new Map(); | ||
| } | ||
| /** | ||
| * Remove and Entry From the Cache | ||
| * @method removeEntry | ||
| * @param filePath - The file path to remove from the cache | ||
| */ | ||
| removeEntry(filePath) { | ||
| const key = this.createFileKey(filePath); | ||
| this._cache.removeKey(key); | ||
| this._originalMeta.delete(key); | ||
| } | ||
| /** | ||
| * Reconcile the cache | ||
| * @method reconcile | ||
| */ | ||
| reconcile() { | ||
| for (const key of [...this._cache.keys()]) try { | ||
| node_fs.default.statSync(this.getAbsolutePath(key)); | ||
| } catch (error) { | ||
| if (error.code === "ENOENT") { | ||
| this._cache.removeKey(key); | ||
| this._originalMeta.delete(key); | ||
| } else this._logger?.error({ | ||
| key, | ||
| error | ||
| }, "reconcile: unable to stat file; keeping cached entry"); | ||
| } | ||
| for (const key of [...this._originalMeta.keys()]) { | ||
| const meta = this._cache.getKey(key); | ||
| if (meta) this._originalMeta.set(key, { ...meta }); | ||
| else this._originalMeta.delete(key); | ||
| } | ||
| this._cache.save(); | ||
| } | ||
| /** | ||
| * Check if the file has changed | ||
| * @method hasFileChanged | ||
| * @param filePath - The file path to check | ||
| * @returns {boolean} if the file has changed, false otherwise | ||
| */ | ||
| hasFileChanged(filePath) { | ||
| let result = false; | ||
| const fileDescriptor = this.getFileDescriptor(filePath); | ||
| if ((!fileDescriptor.err || !fileDescriptor.notFound) && fileDescriptor.changed) result = true; | ||
| return result; | ||
| } | ||
| /** | ||
| * Get the file descriptor for the file path | ||
| * @method getFileDescriptor | ||
| * @param filePath - The file path to get the file descriptor for | ||
| * @param options - The options for getting the file descriptor | ||
| * @returns The file descriptor | ||
| */ | ||
| getFileDescriptor(filePath, options) { | ||
| this._logger?.debug({ | ||
| filePath, | ||
| options | ||
| }, "Getting file descriptor"); | ||
| let fstat; | ||
| const result = { | ||
| key: this.createFileKey(filePath), | ||
| changed: false, | ||
| meta: {} | ||
| }; | ||
| this._logger?.trace({ key: result.key }, "Created file key"); | ||
| const metaCache = this._cache.getKey(result.key); | ||
| if (metaCache) this._logger?.trace({ metaCache }, "Found cached meta"); | ||
| else this._logger?.trace("No cached meta found"); | ||
| result.meta = metaCache ? { ...metaCache } : {}; | ||
| const absolutePath = this.getAbsolutePath(filePath); | ||
| this._logger?.trace({ absolutePath }, "Resolved absolute path"); | ||
| const useCheckSumValue = options?.useCheckSum ?? this._useCheckSum; | ||
| this._logger?.debug({ useCheckSum: useCheckSumValue }, "Using checksum setting"); | ||
| const useModifiedTimeValue = options?.useModifiedTime ?? this.useModifiedTime; | ||
| this._logger?.debug({ useModifiedTime: useModifiedTimeValue }, "Using modified time (mtime) setting"); | ||
| try { | ||
| fstat = node_fs.default.statSync(absolutePath); | ||
| result.meta.size = fstat.size; | ||
| result.meta.mtime = fstat.mtime.getTime(); | ||
| this._logger?.trace({ | ||
| size: result.meta.size, | ||
| mtime: result.meta.mtime | ||
| }, "Read file stats"); | ||
| if (useCheckSumValue) { | ||
| const buffer = node_fs.default.readFileSync(absolutePath); | ||
| result.meta.hash = this.getHash(buffer); | ||
| this._logger?.trace({ hash: result.meta.hash }, "Calculated file hash"); | ||
| } | ||
| } catch (error) { | ||
| this._logger?.error({ | ||
| filePath, | ||
| error | ||
| }, "Error reading file"); | ||
| this.removeEntry(filePath); | ||
| let notFound = false; | ||
| if (error.message.includes("ENOENT")) { | ||
| notFound = true; | ||
| this._logger?.debug({ filePath }, "File not found"); | ||
| } | ||
| return { | ||
| key: result.key, | ||
| err: error, | ||
| notFound, | ||
| meta: {} | ||
| }; | ||
| } | ||
| if (!this._originalMeta.has(result.key)) this._originalMeta.set(result.key, metaCache ? { ...metaCache } : void 0); | ||
| const baseline = this._originalMeta.get(result.key); | ||
| if (baseline === void 0) { | ||
| result.changed = true; | ||
| this._cache.setKey(result.key, result.meta); | ||
| this._logger?.debug({ filePath }, "File not in cache, marked as changed"); | ||
| return result; | ||
| } | ||
| if (useModifiedTimeValue && baseline.mtime !== result.meta?.mtime) { | ||
| result.changed = true; | ||
| this._logger?.debug({ | ||
| filePath, | ||
| oldMtime: baseline.mtime, | ||
| newMtime: result.meta.mtime | ||
| }, "File changed: mtime differs"); | ||
| } | ||
| if (baseline.size !== result.meta?.size) { | ||
| result.changed = true; | ||
| this._logger?.debug({ | ||
| filePath, | ||
| oldSize: baseline.size, | ||
| newSize: result.meta.size | ||
| }, "File changed: size differs"); | ||
| } | ||
| if (useCheckSumValue && baseline.hash !== result.meta?.hash) { | ||
| result.changed = true; | ||
| this._logger?.debug({ | ||
| filePath, | ||
| oldHash: baseline.hash, | ||
| newHash: result.meta.hash | ||
| }, "File changed: hash differs"); | ||
| } | ||
| this._cache.setKey(result.key, result.meta); | ||
| if (result.changed) this._logger?.info({ filePath }, "File has changed"); | ||
| else this._logger?.debug({ filePath }, "File unchanged"); | ||
| return result; | ||
| } | ||
| /** | ||
| * Get the file descriptors for the files | ||
| * @method normalizeEntries | ||
| * @param files?: string[] - The files to get the file descriptors for | ||
| * @returns The file descriptors | ||
| */ | ||
| normalizeEntries(files) { | ||
| const result = []; | ||
| if (files) { | ||
| for (const file of files) { | ||
| const fileDescriptor = this.getFileDescriptor(file); | ||
| result.push(fileDescriptor); | ||
| } | ||
| return result; | ||
| } | ||
| const keys = this.cache.keys(); | ||
| for (const key of keys) { | ||
| const fileDescriptor = this.getFileDescriptor(key); | ||
| if (!fileDescriptor.notFound && !fileDescriptor.err) result.push(fileDescriptor); | ||
| } | ||
| return result; | ||
| } | ||
| /** | ||
| * Analyze the files | ||
| * @method analyzeFiles | ||
| * @param files - The files to analyze | ||
| * @returns {AnalyzedFiles} The analysis of the files | ||
| */ | ||
| analyzeFiles(files) { | ||
| const result = { | ||
| changedFiles: [], | ||
| notFoundFiles: [], | ||
| notChangedFiles: [] | ||
| }; | ||
| const fileDescriptors = this.normalizeEntries(files); | ||
| for (const fileDescriptor of fileDescriptors) if (fileDescriptor.notFound) result.notFoundFiles.push(fileDescriptor.key); | ||
| else if (fileDescriptor.changed) result.changedFiles.push(fileDescriptor.key); | ||
| else result.notChangedFiles.push(fileDescriptor.key); | ||
| return result; | ||
| } | ||
| /** | ||
| * Get the updated files | ||
| * @method getUpdatedFiles | ||
| * @param files - The files to get the updated files for | ||
| * @returns {string[]} The updated files | ||
| */ | ||
| getUpdatedFiles(files) { | ||
| const result = []; | ||
| const fileDescriptors = this.normalizeEntries(files); | ||
| for (const fileDescriptor of fileDescriptors) if (fileDescriptor.changed) result.push(fileDescriptor.key); | ||
| return result; | ||
| } | ||
| /** | ||
| * Get the file descriptors by path prefix | ||
| * @method getFileDescriptorsByPath | ||
| * @param filePath - the path prefix to match | ||
| * @returns {FileDescriptor[]} The file descriptors | ||
| */ | ||
| getFileDescriptorsByPath(filePath) { | ||
| const result = []; | ||
| const keys = this._cache.keys(); | ||
| for (const key of keys) | ||
| /* v8 ignore next -- @preserve */ | ||
| if (key.startsWith(filePath)) { | ||
| const fileDescriptor = this.getFileDescriptor(key); | ||
| result.push(fileDescriptor); | ||
| } | ||
| return result; | ||
| } | ||
| /** | ||
| * Get the Absolute Path. If it is already absolute it will return the path as is. | ||
| * When restrictAccessToCwd is enabled, ensures the resolved path stays within cwd boundaries. | ||
| * @method getAbsolutePath | ||
| * @param filePath - The file path to get the absolute path for | ||
| * @returns {string} | ||
| * @throws {Error} When restrictAccessToCwd is true and path would resolve outside cwd | ||
| */ | ||
| getAbsolutePath(filePath) { | ||
| if (this.isRelativePath(filePath)) { | ||
| const sanitizedPath = filePath.replace(/\0/g, ""); | ||
| const resolved = node_path.default.resolve(this._cwd, sanitizedPath); | ||
| if (this._restrictAccessToCwd) { | ||
| const normalizedResolved = node_path.default.normalize(resolved); | ||
| const normalizedCwd = node_path.default.normalize(this._cwd); | ||
| if (!(normalizedResolved === normalizedCwd || normalizedResolved.startsWith(normalizedCwd + node_path.default.sep))) throw new Error(`Path traversal attempt blocked: "${filePath}" resolves outside of working directory "${this._cwd}"`); | ||
| } | ||
| return resolved; | ||
| } | ||
| return filePath; | ||
| } | ||
| /** | ||
| * Get the Absolute Path with a custom working directory. If it is already absolute it will return the path as is. | ||
| * When restrictAccessToCwd is enabled, ensures the resolved path stays within the provided cwd boundaries. | ||
| * @method getAbsolutePathWithCwd | ||
| * @param filePath - The file path to get the absolute path for | ||
| * @param cwd - The custom working directory to resolve relative paths from | ||
| * @returns {string} | ||
| * @throws {Error} When restrictAccessToCwd is true and path would resolve outside the provided cwd | ||
| */ | ||
| getAbsolutePathWithCwd(filePath, cwd) { | ||
| if (this.isRelativePath(filePath)) { | ||
| const sanitizedPath = filePath.replace(/\0/g, ""); | ||
| const resolved = node_path.default.resolve(cwd, sanitizedPath); | ||
| if (this._restrictAccessToCwd) { | ||
| const normalizedResolved = node_path.default.normalize(resolved); | ||
| const normalizedCwd = node_path.default.normalize(cwd); | ||
| if (!(normalizedResolved === normalizedCwd || normalizedResolved.startsWith(normalizedCwd + node_path.default.sep))) throw new Error(`Path traversal attempt blocked: "${filePath}" resolves outside of working directory "${cwd}"`); | ||
| } | ||
| return resolved; | ||
| } | ||
| return filePath; | ||
| } | ||
| /** | ||
| * Rename cache keys that start with a given path prefix. | ||
| * @method renameCacheKeys | ||
| * @param oldPath - The old path prefix to rename | ||
| * @param newPath - The new path prefix to rename to | ||
| */ | ||
| renameCacheKeys(oldPath, newPath) { | ||
| const keys = this._cache.keys(); | ||
| for (const key of keys) | ||
| /* v8 ignore next -- @preserve */ | ||
| if (key.startsWith(oldPath)) { | ||
| const newKey = key.replace(oldPath, newPath); | ||
| const meta = this._cache.getKey(key); | ||
| this._cache.removeKey(key); | ||
| this._cache.setKey(newKey, meta); | ||
| if (this._originalMeta.has(key)) { | ||
| this._originalMeta.set(newKey, this._originalMeta.get(key)); | ||
| this._originalMeta.delete(key); | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| // Annotate the CommonJS export names for ESM import in node: | ||
| 0 && (module.exports = { | ||
| FileEntryCache, | ||
| create, | ||
| createFromFile | ||
| }); | ||
| /* v8 ignore next -- @preserve */ | ||
| //#endregion | ||
| exports.FileEntryCache = FileEntryCache; | ||
| exports.create = create; | ||
| exports.createFromFile = createFromFile; | ||
| exports.default = FileEntryDefault; |
+257
-271
@@ -1,75 +0,46 @@ | ||
| import { Buffer } from 'node:buffer'; | ||
| import { FlatCacheOptions, FlatCache } from 'flat-cache'; | ||
| import { Buffer } from "node:buffer"; | ||
| import { FlatCache, FlatCacheOptions } from "flat-cache"; | ||
| //#region src/index.d.ts | ||
| type ILogger = { | ||
| /** Current log level */ | ||
| level?: string; | ||
| /** Trace level logging */ | ||
| trace: (message: string | object, ...args: unknown[]) => void; | ||
| /** Debug level logging */ | ||
| debug: (message: string | object, ...args: unknown[]) => void; | ||
| /** Info level logging */ | ||
| info: (message: string | object, ...args: unknown[]) => void; | ||
| /** Warning level logging */ | ||
| warn: (message: string | object, ...args: unknown[]) => void; | ||
| /** Error level logging */ | ||
| error: (message: string | object, ...args: unknown[]) => void; | ||
| /** Fatal level logging */ | ||
| fatal: (message: string | object, ...args: unknown[]) => void; | ||
| /** Current log level */level?: string; /** Trace level logging */ | ||
| trace: (message: string | object, ...args: unknown[]) => void; /** Debug level logging */ | ||
| debug: (message: string | object, ...args: unknown[]) => void; /** Info level logging */ | ||
| info: (message: string | object, ...args: unknown[]) => void; /** Warning level logging */ | ||
| warn: (message: string | object, ...args: unknown[]) => void; /** Error level logging */ | ||
| error: (message: string | object, ...args: unknown[]) => void; /** Fatal level logging */ | ||
| fatal: (message: string | object, ...args: unknown[]) => void; | ||
| }; | ||
| type FileEntryCacheOptions = { | ||
| /** Whether to use file modified time for change detection (default: true) */ | ||
| useModifiedTime?: boolean; | ||
| /** Whether to use checksum for change detection (default: false) */ | ||
| useCheckSum?: boolean; | ||
| /** Hash algorithm to use for checksum (default: 'md5') */ | ||
| hashAlgorithm?: string; | ||
| /** Current working directory for resolving relative paths (default: process.cwd()) */ | ||
| cwd?: string; | ||
| /** Restrict file access to within cwd boundaries (default: true) */ | ||
| restrictAccessToCwd?: boolean; | ||
| /** Whether to use absolute path as cache key (default: false) */ | ||
| useAbsolutePathAsKey?: boolean; | ||
| /** Logger instance for logging (default: undefined) */ | ||
| logger?: ILogger; | ||
| /** Options for the underlying flat cache */ | ||
| cache?: FlatCacheOptions; | ||
| /** Whether to use file modified time for change detection (default: true) */useModifiedTime?: boolean; /** Whether to use checksum for change detection (default: false) */ | ||
| useCheckSum?: boolean; /** Hash algorithm to use for checksum (default: 'md5') */ | ||
| hashAlgorithm?: string; /** Current working directory for resolving relative paths (default: process.cwd()) */ | ||
| cwd?: string; /** Restrict file access to within cwd boundaries (default: true) */ | ||
| restrictAccessToCwd?: boolean; /** Whether to use absolute path as cache key (default: false) */ | ||
| useAbsolutePathAsKey?: boolean; /** Logger instance for logging (default: undefined) */ | ||
| logger?: ILogger; /** Options for the underlying flat cache */ | ||
| cache?: FlatCacheOptions; | ||
| }; | ||
| type GetFileDescriptorOptions = { | ||
| /** Whether to use checksum for this specific file check instead of modified time (mtime) (overrides instance setting) */ | ||
| useCheckSum?: boolean; | ||
| /** Whether to use file modified time for change detection (default: true) */ | ||
| useModifiedTime?: boolean; | ||
| /** Whether to use checksum for this specific file check instead of modified time (mtime) (overrides instance setting) */useCheckSum?: boolean; /** Whether to use file modified time for change detection (default: true) */ | ||
| useModifiedTime?: boolean; | ||
| }; | ||
| type FileDescriptor = { | ||
| /** The cache key for this file (typically the file path) */ | ||
| key: string; | ||
| /** Whether the file has changed since last cache check */ | ||
| changed?: boolean; | ||
| /** Metadata about the file */ | ||
| meta: FileDescriptorMeta; | ||
| /** Whether the file was not found */ | ||
| notFound?: boolean; | ||
| /** Error encountered when accessing the file */ | ||
| err?: Error; | ||
| /** The cache key for this file (typically the file path) */key: string; /** Whether the file has changed since last cache check */ | ||
| changed?: boolean; /** Metadata about the file */ | ||
| meta: FileDescriptorMeta; /** Whether the file was not found */ | ||
| notFound?: boolean; /** Error encountered when accessing the file */ | ||
| err?: Error; | ||
| }; | ||
| type FileDescriptorMeta = { | ||
| /** File size in bytes */ | ||
| size?: number; | ||
| /** File modification time (timestamp in milliseconds) */ | ||
| mtime?: number; | ||
| /** File content hash (when useCheckSum is enabled) */ | ||
| hash?: string; | ||
| /** Custom data associated with the file (e.g., lint results, metadata) */ | ||
| data?: unknown; | ||
| /** Allow any additional custom properties */ | ||
| [key: string]: unknown; | ||
| /** File size in bytes */size?: number; /** File modification time (timestamp in milliseconds) */ | ||
| mtime?: number; /** File content hash (when useCheckSum is enabled) */ | ||
| hash?: string; /** Custom data associated with the file (e.g., lint results, metadata) */ | ||
| data?: unknown; /** Allow any additional custom properties */ | ||
| [key: string]: unknown; | ||
| }; | ||
| type AnalyzedFiles = { | ||
| /** Array of file paths that have changed since last cache */ | ||
| changedFiles: string[]; | ||
| /** Array of file paths that were not found */ | ||
| notFoundFiles: string[]; | ||
| /** Array of file paths that have not changed since last cache */ | ||
| notChangedFiles: string[]; | ||
| /** Array of file paths that have changed since last cache */changedFiles: string[]; /** Array of file paths that were not found */ | ||
| notFoundFiles: string[]; /** Array of file paths that have not changed since last cache */ | ||
| notChangedFiles: string[]; | ||
| }; | ||
@@ -93,213 +64,228 @@ /** | ||
| declare class FileEntryDefault { | ||
| static create: typeof create; | ||
| static createFromFile: typeof createFromFile; | ||
| static create: typeof create; | ||
| static createFromFile: typeof createFromFile; | ||
| } | ||
| declare class FileEntryCache { | ||
| private _cache; | ||
| private _useCheckSum; | ||
| private _hashAlgorithm; | ||
| private _cwd; | ||
| private _restrictAccessToCwd; | ||
| private _logger?; | ||
| private _useAbsolutePathAsKey; | ||
| private _useModifiedTime; | ||
| /** | ||
| * Create a new FileEntryCache instance | ||
| * @param options - The options for the FileEntryCache (all properties are optional with defaults) | ||
| */ | ||
| constructor(options?: FileEntryCacheOptions); | ||
| /** | ||
| * Get the cache | ||
| * @returns {FlatCache} The cache | ||
| */ | ||
| get cache(): FlatCache; | ||
| /** | ||
| * Set the cache | ||
| * @param {FlatCache} cache - The cache to set | ||
| */ | ||
| set cache(cache: FlatCache); | ||
| /** | ||
| * Get the logger | ||
| * @returns {ILogger | undefined} The logger instance | ||
| */ | ||
| get logger(): ILogger | undefined; | ||
| /** | ||
| * Set the logger | ||
| * @param {ILogger | undefined} logger - The logger to set | ||
| */ | ||
| set logger(logger: ILogger | undefined); | ||
| /** | ||
| * Use the hash to check if the file has changed | ||
| * @returns {boolean} if the hash is used to check if the file has changed (default: false) | ||
| */ | ||
| get useCheckSum(): boolean; | ||
| /** | ||
| * Set the useCheckSum value | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set useCheckSum(value: boolean); | ||
| /** | ||
| * Get the hash algorithm | ||
| * @returns {string} The hash algorithm (default: 'md5') | ||
| */ | ||
| get hashAlgorithm(): string; | ||
| /** | ||
| * Set the hash algorithm | ||
| * @param {string} value - The value to set | ||
| */ | ||
| set hashAlgorithm(value: string); | ||
| /** | ||
| * Get the current working directory | ||
| * @returns {string} The current working directory (default: process.cwd()) | ||
| */ | ||
| get cwd(): string; | ||
| /** | ||
| * Set the current working directory | ||
| * @param {string} value - The value to set | ||
| */ | ||
| set cwd(value: string); | ||
| /** | ||
| * Get whether to use modified time for change detection | ||
| * @returns {boolean} Whether modified time (mtime) is used for change detection (default: true) | ||
| */ | ||
| get useModifiedTime(): boolean; | ||
| /** | ||
| * Set whether to use modified time for change detection | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set useModifiedTime(value: boolean); | ||
| /** | ||
| * Get whether to restrict paths to cwd boundaries | ||
| * @returns {boolean} Whether strict path checking is enabled (default: true) | ||
| */ | ||
| get restrictAccessToCwd(): boolean; | ||
| /** | ||
| * Set whether to restrict paths to cwd boundaries | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set restrictAccessToCwd(value: boolean); | ||
| /** | ||
| * Get whether to use absolute path as cache key | ||
| * @returns {boolean} Whether cache keys use absolute paths (default: false) | ||
| */ | ||
| get useAbsolutePathAsKey(): boolean; | ||
| /** | ||
| * Set whether to use absolute path as cache key | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set useAbsolutePathAsKey(value: boolean); | ||
| /** | ||
| * Given a buffer, calculate md5 hash of its content. | ||
| * @method getHash | ||
| * @param {Buffer} buffer buffer to calculate hash on | ||
| * @return {String} content hash digest | ||
| */ | ||
| getHash(buffer: Buffer): string; | ||
| /** | ||
| * Create the key for the file path used for caching. | ||
| * @method createFileKey | ||
| * @param {String} filePath | ||
| * @return {String} | ||
| */ | ||
| createFileKey(filePath: string): string; | ||
| /** | ||
| * Check if the file path is a relative path | ||
| * @method isRelativePath | ||
| * @param filePath - The file path to check | ||
| * @returns {boolean} if the file path is a relative path, false otherwise | ||
| */ | ||
| isRelativePath(filePath: string): boolean; | ||
| /** | ||
| * Delete the cache file from the disk | ||
| * @method deleteCacheFile | ||
| * @return {boolean} true if the file was deleted, false otherwise | ||
| */ | ||
| deleteCacheFile(): boolean; | ||
| /** | ||
| * Remove the cache from the file and clear the memory cache | ||
| * @method destroy | ||
| */ | ||
| destroy(): void; | ||
| /** | ||
| * Remove and Entry From the Cache | ||
| * @method removeEntry | ||
| * @param filePath - The file path to remove from the cache | ||
| */ | ||
| removeEntry(filePath: string): void; | ||
| /** | ||
| * Reconcile the cache | ||
| * @method reconcile | ||
| */ | ||
| reconcile(): void; | ||
| /** | ||
| * Check if the file has changed | ||
| * @method hasFileChanged | ||
| * @param filePath - The file path to check | ||
| * @returns {boolean} if the file has changed, false otherwise | ||
| */ | ||
| hasFileChanged(filePath: string): boolean; | ||
| /** | ||
| * Get the file descriptor for the file path | ||
| * @method getFileDescriptor | ||
| * @param filePath - The file path to get the file descriptor for | ||
| * @param options - The options for getting the file descriptor | ||
| * @returns The file descriptor | ||
| */ | ||
| getFileDescriptor(filePath: string, options?: GetFileDescriptorOptions): FileDescriptor; | ||
| /** | ||
| * Get the file descriptors for the files | ||
| * @method normalizeEntries | ||
| * @param files?: string[] - The files to get the file descriptors for | ||
| * @returns The file descriptors | ||
| */ | ||
| normalizeEntries(files?: string[]): FileDescriptor[]; | ||
| /** | ||
| * Analyze the files | ||
| * @method analyzeFiles | ||
| * @param files - The files to analyze | ||
| * @returns {AnalyzedFiles} The analysis of the files | ||
| */ | ||
| analyzeFiles(files: string[]): AnalyzedFiles; | ||
| /** | ||
| * Get the updated files | ||
| * @method getUpdatedFiles | ||
| * @param files - The files to get the updated files for | ||
| * @returns {string[]} The updated files | ||
| */ | ||
| getUpdatedFiles(files: string[]): string[]; | ||
| /** | ||
| * Get the file descriptors by path prefix | ||
| * @method getFileDescriptorsByPath | ||
| * @param filePath - the path prefix to match | ||
| * @returns {FileDescriptor[]} The file descriptors | ||
| */ | ||
| getFileDescriptorsByPath(filePath: string): FileDescriptor[]; | ||
| /** | ||
| * Get the Absolute Path. If it is already absolute it will return the path as is. | ||
| * When restrictAccessToCwd is enabled, ensures the resolved path stays within cwd boundaries. | ||
| * @method getAbsolutePath | ||
| * @param filePath - The file path to get the absolute path for | ||
| * @returns {string} | ||
| * @throws {Error} When restrictAccessToCwd is true and path would resolve outside cwd | ||
| */ | ||
| getAbsolutePath(filePath: string): string; | ||
| /** | ||
| * Get the Absolute Path with a custom working directory. If it is already absolute it will return the path as is. | ||
| * When restrictAccessToCwd is enabled, ensures the resolved path stays within the provided cwd boundaries. | ||
| * @method getAbsolutePathWithCwd | ||
| * @param filePath - The file path to get the absolute path for | ||
| * @param cwd - The custom working directory to resolve relative paths from | ||
| * @returns {string} | ||
| * @throws {Error} When restrictAccessToCwd is true and path would resolve outside the provided cwd | ||
| */ | ||
| getAbsolutePathWithCwd(filePath: string, cwd: string): string; | ||
| /** | ||
| * Rename cache keys that start with a given path prefix. | ||
| * @method renameCacheKeys | ||
| * @param oldPath - The old path prefix to rename | ||
| * @param newPath - The new path prefix to rename to | ||
| */ | ||
| renameCacheKeys(oldPath: string, newPath: string): void; | ||
| private _cache; | ||
| private _useCheckSum; | ||
| private _hashAlgorithm; | ||
| private _cwd; | ||
| private _restrictAccessToCwd; | ||
| private _logger?; | ||
| private _useAbsolutePathAsKey; | ||
| private _useModifiedTime; | ||
| /** | ||
| * Snapshot of the persisted meta for each key as of the last load/reconcile. | ||
| * Change detection compares against this baseline (not the working cache) so | ||
| * that repeated `getFileDescriptor()` calls keep reporting a file as changed | ||
| * until the cache is reconciled. The set of keys also tracks which files were | ||
| * visited during the current session so that `reconcile()` only updates those. | ||
| */ | ||
| private _originalMeta; | ||
| /** | ||
| * Create a new FileEntryCache instance | ||
| * @param options - The options for the FileEntryCache (all properties are optional with defaults) | ||
| */ | ||
| constructor(options?: FileEntryCacheOptions); | ||
| /** | ||
| * Get the cache | ||
| * @returns {FlatCache} The cache | ||
| */ | ||
| get cache(): FlatCache; | ||
| /** | ||
| * Set the cache | ||
| * @param {FlatCache} cache - The cache to set | ||
| */ | ||
| set cache(cache: FlatCache); | ||
| /** | ||
| * Get the logger | ||
| * @returns {ILogger | undefined} The logger instance | ||
| */ | ||
| get logger(): ILogger | undefined; | ||
| /** | ||
| * Set the logger | ||
| * @param {ILogger | undefined} logger - The logger to set | ||
| */ | ||
| set logger(logger: ILogger | undefined); | ||
| /** | ||
| * Use the hash to check if the file has changed | ||
| * @returns {boolean} if the hash is used to check if the file has changed (default: false) | ||
| */ | ||
| get useCheckSum(): boolean; | ||
| /** | ||
| * Set the useCheckSum value | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set useCheckSum(value: boolean); | ||
| /** | ||
| * Get the hash algorithm | ||
| * @returns {string} The hash algorithm (default: 'md5') | ||
| */ | ||
| get hashAlgorithm(): string; | ||
| /** | ||
| * Set the hash algorithm | ||
| * @param {string} value - The value to set | ||
| */ | ||
| set hashAlgorithm(value: string); | ||
| /** | ||
| * Get the current working directory | ||
| * @returns {string} The current working directory (default: process.cwd()) | ||
| */ | ||
| get cwd(): string; | ||
| /** | ||
| * Set the current working directory | ||
| * | ||
| * Note: when relative paths are used as cache keys (the default), `cwd` must | ||
| * stay stable across a `getFileDescriptor()` / `reconcile()` cycle. Relative | ||
| * keys are resolved against the *current* `cwd` each time, so changing it | ||
| * mid-run can cause `reconcile()` to resolve a key to a different (missing) | ||
| * path and drop the entry. Use absolute keys (`useAbsolutePathAsKey: true`) | ||
| * if `cwd` must change during a run. | ||
| * @param {string} value - The value to set | ||
| */ | ||
| set cwd(value: string); | ||
| /** | ||
| * Get whether to use modified time for change detection | ||
| * @returns {boolean} Whether modified time (mtime) is used for change detection (default: true) | ||
| */ | ||
| get useModifiedTime(): boolean; | ||
| /** | ||
| * Set whether to use modified time for change detection | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set useModifiedTime(value: boolean); | ||
| /** | ||
| * Get whether to restrict paths to cwd boundaries | ||
| * @returns {boolean} Whether strict path checking is enabled (default: true) | ||
| */ | ||
| get restrictAccessToCwd(): boolean; | ||
| /** | ||
| * Set whether to restrict paths to cwd boundaries | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set restrictAccessToCwd(value: boolean); | ||
| /** | ||
| * Get whether to use absolute path as cache key | ||
| * @returns {boolean} Whether cache keys use absolute paths (default: false) | ||
| */ | ||
| get useAbsolutePathAsKey(): boolean; | ||
| /** | ||
| * Set whether to use absolute path as cache key | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set useAbsolutePathAsKey(value: boolean); | ||
| /** | ||
| * Given a buffer, calculate md5 hash of its content. | ||
| * @method getHash | ||
| * @param {Buffer} buffer buffer to calculate hash on | ||
| * @return {String} content hash digest | ||
| */ | ||
| getHash(buffer: Buffer): string; | ||
| /** | ||
| * Create the key for the file path used for caching. | ||
| * @method createFileKey | ||
| * @param {String} filePath | ||
| * @return {String} | ||
| */ | ||
| createFileKey(filePath: string): string; | ||
| /** | ||
| * Check if the file path is a relative path | ||
| * @method isRelativePath | ||
| * @param filePath - The file path to check | ||
| * @returns {boolean} if the file path is a relative path, false otherwise | ||
| */ | ||
| isRelativePath(filePath: string): boolean; | ||
| /** | ||
| * Delete the cache file from the disk | ||
| * @method deleteCacheFile | ||
| * @return {boolean} true if the file was deleted, false otherwise | ||
| */ | ||
| deleteCacheFile(): boolean; | ||
| /** | ||
| * Remove the cache from the file and clear the memory cache | ||
| * @method destroy | ||
| */ | ||
| destroy(): void; | ||
| /** | ||
| * Remove and Entry From the Cache | ||
| * @method removeEntry | ||
| * @param filePath - The file path to remove from the cache | ||
| */ | ||
| removeEntry(filePath: string): void; | ||
| /** | ||
| * Reconcile the cache | ||
| * @method reconcile | ||
| */ | ||
| reconcile(): void; | ||
| /** | ||
| * Check if the file has changed | ||
| * @method hasFileChanged | ||
| * @param filePath - The file path to check | ||
| * @returns {boolean} if the file has changed, false otherwise | ||
| */ | ||
| hasFileChanged(filePath: string): boolean; | ||
| /** | ||
| * Get the file descriptor for the file path | ||
| * @method getFileDescriptor | ||
| * @param filePath - The file path to get the file descriptor for | ||
| * @param options - The options for getting the file descriptor | ||
| * @returns The file descriptor | ||
| */ | ||
| getFileDescriptor(filePath: string, options?: GetFileDescriptorOptions): FileDescriptor; | ||
| /** | ||
| * Get the file descriptors for the files | ||
| * @method normalizeEntries | ||
| * @param files?: string[] - The files to get the file descriptors for | ||
| * @returns The file descriptors | ||
| */ | ||
| normalizeEntries(files?: string[]): FileDescriptor[]; | ||
| /** | ||
| * Analyze the files | ||
| * @method analyzeFiles | ||
| * @param files - The files to analyze | ||
| * @returns {AnalyzedFiles} The analysis of the files | ||
| */ | ||
| analyzeFiles(files: string[]): AnalyzedFiles; | ||
| /** | ||
| * Get the updated files | ||
| * @method getUpdatedFiles | ||
| * @param files - The files to get the updated files for | ||
| * @returns {string[]} The updated files | ||
| */ | ||
| getUpdatedFiles(files: string[]): string[]; | ||
| /** | ||
| * Get the file descriptors by path prefix | ||
| * @method getFileDescriptorsByPath | ||
| * @param filePath - the path prefix to match | ||
| * @returns {FileDescriptor[]} The file descriptors | ||
| */ | ||
| getFileDescriptorsByPath(filePath: string): FileDescriptor[]; | ||
| /** | ||
| * Get the Absolute Path. If it is already absolute it will return the path as is. | ||
| * When restrictAccessToCwd is enabled, ensures the resolved path stays within cwd boundaries. | ||
| * @method getAbsolutePath | ||
| * @param filePath - The file path to get the absolute path for | ||
| * @returns {string} | ||
| * @throws {Error} When restrictAccessToCwd is true and path would resolve outside cwd | ||
| */ | ||
| getAbsolutePath(filePath: string): string; | ||
| /** | ||
| * Get the Absolute Path with a custom working directory. If it is already absolute it will return the path as is. | ||
| * When restrictAccessToCwd is enabled, ensures the resolved path stays within the provided cwd boundaries. | ||
| * @method getAbsolutePathWithCwd | ||
| * @param filePath - The file path to get the absolute path for | ||
| * @param cwd - The custom working directory to resolve relative paths from | ||
| * @returns {string} | ||
| * @throws {Error} When restrictAccessToCwd is true and path would resolve outside the provided cwd | ||
| */ | ||
| getAbsolutePathWithCwd(filePath: string, cwd: string): string; | ||
| /** | ||
| * Rename cache keys that start with a given path prefix. | ||
| * @method renameCacheKeys | ||
| * @param oldPath - The old path prefix to rename | ||
| * @param newPath - The new path prefix to rename to | ||
| */ | ||
| renameCacheKeys(oldPath: string, newPath: string): void; | ||
| } | ||
| export { type AnalyzedFiles, type CreateOptions, type FileDescriptor, type FileDescriptorMeta, FileEntryCache, type FileEntryCacheOptions, type GetFileDescriptorOptions, type ILogger, create, createFromFile, FileEntryDefault as default }; | ||
| //#endregion | ||
| export { AnalyzedFiles, CreateOptions, FileDescriptor, FileDescriptorMeta, FileEntryCache, FileEntryCacheOptions, GetFileDescriptorOptions, ILogger, create, createFromFile, FileEntryDefault as default }; |
+9
-9
| { | ||
| "name": "file-entry-cache", | ||
| "version": "11.1.3", | ||
| "version": "11.1.5", | ||
| "description": "A lightweight cache for file metadata, ideal for processes that work on a specific set of files and only need to reprocess files that have changed since the last run", | ||
| "type": "module", | ||
| "main": "./dist/index.js", | ||
| "module": "./dist/index.js", | ||
| "types": "./dist/index.d.ts", | ||
| "main": "./dist/index.cjs", | ||
| "module": "./dist/index.mjs", | ||
| "types": "./dist/index.d.mts", | ||
| "exports": { | ||
| ".": { | ||
| "import": { | ||
| "types": "./dist/index.d.ts", | ||
| "default": "./dist/index.js" | ||
| "types": "./dist/index.d.mts", | ||
| "default": "./dist/index.mjs" | ||
| }, | ||
@@ -39,7 +39,7 @@ "require": { | ||
| "pino": "^10.3.1", | ||
| "tsup": "^8.5.1", | ||
| "tsdown": "^0.22.0", | ||
| "typescript": "^5.9.3" | ||
| }, | ||
| "dependencies": { | ||
| "flat-cache": "^6.1.22" | ||
| "flat-cache": "^6.1.23" | ||
| }, | ||
@@ -51,3 +51,3 @@ "files": [ | ||
| "scripts": { | ||
| "build": "rimraf ./dist && tsup src/index.ts --format cjs,esm --dts --clean", | ||
| "build": "rimraf ./dist && tsdown src/index.ts --format cjs,esm --dts --clean", | ||
| "lint": "biome check --write --error-on-warnings", | ||
@@ -54,0 +54,0 @@ "test": "pnpm lint && vitest run --coverage", |
+5
-1
@@ -64,4 +64,8 @@ [<img align="center" src="https://cacheable.org/symbol.svg" alt="Cacheable" />](https://github.com/jaredwray/cacheable) | ||
| // Repeated calls keep reporting `changed: true` until you persist the state | ||
| // with reconcile(); only then does the file become the cached baseline. | ||
| cache.reconcile(); | ||
| fileDescriptor = cache.getFileDescriptor('./src/file.txt'); | ||
| console.log(fileDescriptor.changed); // false as it has not changed | ||
| console.log(fileDescriptor.changed); // false as it has not changed since reconcile() | ||
@@ -68,0 +72,0 @@ // do something to change the file |
-304
| import { Buffer } from 'node:buffer'; | ||
| import { FlatCacheOptions, FlatCache } from 'flat-cache'; | ||
| type ILogger = { | ||
| /** Current log level */ | ||
| level?: string; | ||
| /** Trace level logging */ | ||
| trace: (message: string | object, ...args: unknown[]) => void; | ||
| /** Debug level logging */ | ||
| debug: (message: string | object, ...args: unknown[]) => void; | ||
| /** Info level logging */ | ||
| info: (message: string | object, ...args: unknown[]) => void; | ||
| /** Warning level logging */ | ||
| warn: (message: string | object, ...args: unknown[]) => void; | ||
| /** Error level logging */ | ||
| error: (message: string | object, ...args: unknown[]) => void; | ||
| /** Fatal level logging */ | ||
| fatal: (message: string | object, ...args: unknown[]) => void; | ||
| }; | ||
| type FileEntryCacheOptions = { | ||
| /** Whether to use file modified time for change detection (default: true) */ | ||
| useModifiedTime?: boolean; | ||
| /** Whether to use checksum for change detection (default: false) */ | ||
| useCheckSum?: boolean; | ||
| /** Hash algorithm to use for checksum (default: 'md5') */ | ||
| hashAlgorithm?: string; | ||
| /** Current working directory for resolving relative paths (default: process.cwd()) */ | ||
| cwd?: string; | ||
| /** Restrict file access to within cwd boundaries (default: true) */ | ||
| restrictAccessToCwd?: boolean; | ||
| /** Whether to use absolute path as cache key (default: false) */ | ||
| useAbsolutePathAsKey?: boolean; | ||
| /** Logger instance for logging (default: undefined) */ | ||
| logger?: ILogger; | ||
| /** Options for the underlying flat cache */ | ||
| cache?: FlatCacheOptions; | ||
| }; | ||
| type GetFileDescriptorOptions = { | ||
| /** Whether to use checksum for this specific file check instead of modified time (mtime) (overrides instance setting) */ | ||
| useCheckSum?: boolean; | ||
| /** Whether to use file modified time for change detection (default: true) */ | ||
| useModifiedTime?: boolean; | ||
| }; | ||
| type FileDescriptor = { | ||
| /** The cache key for this file (typically the file path) */ | ||
| key: string; | ||
| /** Whether the file has changed since last cache check */ | ||
| changed?: boolean; | ||
| /** Metadata about the file */ | ||
| meta: FileDescriptorMeta; | ||
| /** Whether the file was not found */ | ||
| notFound?: boolean; | ||
| /** Error encountered when accessing the file */ | ||
| err?: Error; | ||
| }; | ||
| type FileDescriptorMeta = { | ||
| /** File size in bytes */ | ||
| size?: number; | ||
| /** File modification time (timestamp in milliseconds) */ | ||
| mtime?: number; | ||
| /** File content hash (when useCheckSum is enabled) */ | ||
| hash?: string; | ||
| /** Custom data associated with the file (e.g., lint results, metadata) */ | ||
| data?: unknown; | ||
| /** Allow any additional custom properties */ | ||
| [key: string]: unknown; | ||
| }; | ||
| type AnalyzedFiles = { | ||
| /** Array of file paths that have changed since last cache */ | ||
| changedFiles: string[]; | ||
| /** Array of file paths that were not found */ | ||
| notFoundFiles: string[]; | ||
| /** Array of file paths that have not changed since last cache */ | ||
| notChangedFiles: string[]; | ||
| }; | ||
| /** | ||
| * Create a new FileEntryCache instance from a file path | ||
| * @param filePath - The path to the cache file | ||
| * @param options - create options such as useChecksum, cwd, and more | ||
| * @returns A new FileEntryCache instance | ||
| */ | ||
| declare function createFromFile(filePath: string, options?: CreateOptions): FileEntryCache; | ||
| type CreateOptions = Omit<FileEntryCacheOptions, "cache">; | ||
| /** | ||
| * Create a new FileEntryCache instance | ||
| * @param cacheId - The cache file name | ||
| * @param cacheDirectory - The directory to store the cache file (default: undefined, cache won't be persisted) | ||
| * @param options - Whether to use checksum to detect file changes (default: false) | ||
| * @returns A new FileEntryCache instance | ||
| */ | ||
| declare function create(cacheId: string, cacheDirectory?: string, options?: CreateOptions): FileEntryCache; | ||
| declare class FileEntryDefault { | ||
| static create: typeof create; | ||
| static createFromFile: typeof createFromFile; | ||
| } | ||
| declare class FileEntryCache { | ||
| private _cache; | ||
| private _useCheckSum; | ||
| private _hashAlgorithm; | ||
| private _cwd; | ||
| private _restrictAccessToCwd; | ||
| private _logger?; | ||
| private _useAbsolutePathAsKey; | ||
| private _useModifiedTime; | ||
| /** | ||
| * Create a new FileEntryCache instance | ||
| * @param options - The options for the FileEntryCache (all properties are optional with defaults) | ||
| */ | ||
| constructor(options?: FileEntryCacheOptions); | ||
| /** | ||
| * Get the cache | ||
| * @returns {FlatCache} The cache | ||
| */ | ||
| get cache(): FlatCache; | ||
| /** | ||
| * Set the cache | ||
| * @param {FlatCache} cache - The cache to set | ||
| */ | ||
| set cache(cache: FlatCache); | ||
| /** | ||
| * Get the logger | ||
| * @returns {ILogger | undefined} The logger instance | ||
| */ | ||
| get logger(): ILogger | undefined; | ||
| /** | ||
| * Set the logger | ||
| * @param {ILogger | undefined} logger - The logger to set | ||
| */ | ||
| set logger(logger: ILogger | undefined); | ||
| /** | ||
| * Use the hash to check if the file has changed | ||
| * @returns {boolean} if the hash is used to check if the file has changed (default: false) | ||
| */ | ||
| get useCheckSum(): boolean; | ||
| /** | ||
| * Set the useCheckSum value | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set useCheckSum(value: boolean); | ||
| /** | ||
| * Get the hash algorithm | ||
| * @returns {string} The hash algorithm (default: 'md5') | ||
| */ | ||
| get hashAlgorithm(): string; | ||
| /** | ||
| * Set the hash algorithm | ||
| * @param {string} value - The value to set | ||
| */ | ||
| set hashAlgorithm(value: string); | ||
| /** | ||
| * Get the current working directory | ||
| * @returns {string} The current working directory (default: process.cwd()) | ||
| */ | ||
| get cwd(): string; | ||
| /** | ||
| * Set the current working directory | ||
| * @param {string} value - The value to set | ||
| */ | ||
| set cwd(value: string); | ||
| /** | ||
| * Get whether to use modified time for change detection | ||
| * @returns {boolean} Whether modified time (mtime) is used for change detection (default: true) | ||
| */ | ||
| get useModifiedTime(): boolean; | ||
| /** | ||
| * Set whether to use modified time for change detection | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set useModifiedTime(value: boolean); | ||
| /** | ||
| * Get whether to restrict paths to cwd boundaries | ||
| * @returns {boolean} Whether strict path checking is enabled (default: true) | ||
| */ | ||
| get restrictAccessToCwd(): boolean; | ||
| /** | ||
| * Set whether to restrict paths to cwd boundaries | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set restrictAccessToCwd(value: boolean); | ||
| /** | ||
| * Get whether to use absolute path as cache key | ||
| * @returns {boolean} Whether cache keys use absolute paths (default: false) | ||
| */ | ||
| get useAbsolutePathAsKey(): boolean; | ||
| /** | ||
| * Set whether to use absolute path as cache key | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set useAbsolutePathAsKey(value: boolean); | ||
| /** | ||
| * Given a buffer, calculate md5 hash of its content. | ||
| * @method getHash | ||
| * @param {Buffer} buffer buffer to calculate hash on | ||
| * @return {String} content hash digest | ||
| */ | ||
| getHash(buffer: Buffer): string; | ||
| /** | ||
| * Create the key for the file path used for caching. | ||
| * @method createFileKey | ||
| * @param {String} filePath | ||
| * @return {String} | ||
| */ | ||
| createFileKey(filePath: string): string; | ||
| /** | ||
| * Check if the file path is a relative path | ||
| * @method isRelativePath | ||
| * @param filePath - The file path to check | ||
| * @returns {boolean} if the file path is a relative path, false otherwise | ||
| */ | ||
| isRelativePath(filePath: string): boolean; | ||
| /** | ||
| * Delete the cache file from the disk | ||
| * @method deleteCacheFile | ||
| * @return {boolean} true if the file was deleted, false otherwise | ||
| */ | ||
| deleteCacheFile(): boolean; | ||
| /** | ||
| * Remove the cache from the file and clear the memory cache | ||
| * @method destroy | ||
| */ | ||
| destroy(): void; | ||
| /** | ||
| * Remove and Entry From the Cache | ||
| * @method removeEntry | ||
| * @param filePath - The file path to remove from the cache | ||
| */ | ||
| removeEntry(filePath: string): void; | ||
| /** | ||
| * Reconcile the cache | ||
| * @method reconcile | ||
| */ | ||
| reconcile(): void; | ||
| /** | ||
| * Check if the file has changed | ||
| * @method hasFileChanged | ||
| * @param filePath - The file path to check | ||
| * @returns {boolean} if the file has changed, false otherwise | ||
| */ | ||
| hasFileChanged(filePath: string): boolean; | ||
| /** | ||
| * Get the file descriptor for the file path | ||
| * @method getFileDescriptor | ||
| * @param filePath - The file path to get the file descriptor for | ||
| * @param options - The options for getting the file descriptor | ||
| * @returns The file descriptor | ||
| */ | ||
| getFileDescriptor(filePath: string, options?: GetFileDescriptorOptions): FileDescriptor; | ||
| /** | ||
| * Get the file descriptors for the files | ||
| * @method normalizeEntries | ||
| * @param files?: string[] - The files to get the file descriptors for | ||
| * @returns The file descriptors | ||
| */ | ||
| normalizeEntries(files?: string[]): FileDescriptor[]; | ||
| /** | ||
| * Analyze the files | ||
| * @method analyzeFiles | ||
| * @param files - The files to analyze | ||
| * @returns {AnalyzedFiles} The analysis of the files | ||
| */ | ||
| analyzeFiles(files: string[]): AnalyzedFiles; | ||
| /** | ||
| * Get the updated files | ||
| * @method getUpdatedFiles | ||
| * @param files - The files to get the updated files for | ||
| * @returns {string[]} The updated files | ||
| */ | ||
| getUpdatedFiles(files: string[]): string[]; | ||
| /** | ||
| * Get the file descriptors by path prefix | ||
| * @method getFileDescriptorsByPath | ||
| * @param filePath - the path prefix to match | ||
| * @returns {FileDescriptor[]} The file descriptors | ||
| */ | ||
| getFileDescriptorsByPath(filePath: string): FileDescriptor[]; | ||
| /** | ||
| * Get the Absolute Path. If it is already absolute it will return the path as is. | ||
| * When restrictAccessToCwd is enabled, ensures the resolved path stays within cwd boundaries. | ||
| * @method getAbsolutePath | ||
| * @param filePath - The file path to get the absolute path for | ||
| * @returns {string} | ||
| * @throws {Error} When restrictAccessToCwd is true and path would resolve outside cwd | ||
| */ | ||
| getAbsolutePath(filePath: string): string; | ||
| /** | ||
| * Get the Absolute Path with a custom working directory. If it is already absolute it will return the path as is. | ||
| * When restrictAccessToCwd is enabled, ensures the resolved path stays within the provided cwd boundaries. | ||
| * @method getAbsolutePathWithCwd | ||
| * @param filePath - The file path to get the absolute path for | ||
| * @param cwd - The custom working directory to resolve relative paths from | ||
| * @returns {string} | ||
| * @throws {Error} When restrictAccessToCwd is true and path would resolve outside the provided cwd | ||
| */ | ||
| getAbsolutePathWithCwd(filePath: string, cwd: string): string; | ||
| /** | ||
| * Rename cache keys that start with a given path prefix. | ||
| * @method renameCacheKeys | ||
| * @param oldPath - The old path prefix to rename | ||
| * @param newPath - The new path prefix to rename to | ||
| */ | ||
| renameCacheKeys(oldPath: string, newPath: string): void; | ||
| } | ||
| export { type AnalyzedFiles, type CreateOptions, type FileDescriptor, type FileDescriptorMeta, FileEntryCache, type FileEntryCacheOptions, type GetFileDescriptorOptions, type ILogger, create, createFromFile, FileEntryDefault as default }; |
-525
| // src/index.ts | ||
| import crypto from "crypto"; | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| import { | ||
| createFromFile as createFlatCacheFile, | ||
| FlatCache | ||
| } from "flat-cache"; | ||
| function createFromFile(filePath, options) { | ||
| const fname = path.basename(filePath); | ||
| const directory = path.dirname(filePath); | ||
| return create(fname, directory, options); | ||
| } | ||
| function create(cacheId, cacheDirectory, options) { | ||
| const opts = { | ||
| ...options, | ||
| cache: { | ||
| cacheId, | ||
| cacheDir: cacheDirectory | ||
| } | ||
| }; | ||
| const fileEntryCache = new FileEntryCache(opts); | ||
| if (cacheDirectory) { | ||
| const cachePath = `${cacheDirectory}/${cacheId}`; | ||
| if (fs.existsSync(cachePath)) { | ||
| fileEntryCache.cache = createFlatCacheFile(cachePath, opts.cache); | ||
| } | ||
| } | ||
| return fileEntryCache; | ||
| } | ||
| var FileEntryDefault = class { | ||
| static create = create; | ||
| static createFromFile = createFromFile; | ||
| }; | ||
| var FileEntryCache = class { | ||
| _cache = new FlatCache({ useClone: false }); | ||
| _useCheckSum = false; | ||
| _hashAlgorithm = "md5"; | ||
| _cwd = process.cwd(); | ||
| _restrictAccessToCwd = false; | ||
| _logger; | ||
| _useAbsolutePathAsKey = false; | ||
| _useModifiedTime = true; | ||
| /** | ||
| * Create a new FileEntryCache instance | ||
| * @param options - The options for the FileEntryCache (all properties are optional with defaults) | ||
| */ | ||
| constructor(options) { | ||
| if (options?.cache) { | ||
| this._cache = new FlatCache(options.cache); | ||
| } | ||
| if (options?.useCheckSum) { | ||
| this._useCheckSum = options.useCheckSum; | ||
| } | ||
| if (options?.hashAlgorithm) { | ||
| this._hashAlgorithm = options.hashAlgorithm; | ||
| } | ||
| if (options?.cwd) { | ||
| this._cwd = options.cwd; | ||
| } | ||
| if (options?.useModifiedTime !== void 0) { | ||
| this._useModifiedTime = options.useModifiedTime; | ||
| } | ||
| if (options?.restrictAccessToCwd !== void 0) { | ||
| this._restrictAccessToCwd = options.restrictAccessToCwd; | ||
| } | ||
| if (options?.useAbsolutePathAsKey !== void 0) { | ||
| this._useAbsolutePathAsKey = options.useAbsolutePathAsKey; | ||
| } | ||
| if (options?.logger) { | ||
| this._logger = options.logger; | ||
| } | ||
| } | ||
| /** | ||
| * Get the cache | ||
| * @returns {FlatCache} The cache | ||
| */ | ||
| get cache() { | ||
| return this._cache; | ||
| } | ||
| /** | ||
| * Set the cache | ||
| * @param {FlatCache} cache - The cache to set | ||
| */ | ||
| set cache(cache) { | ||
| this._cache = cache; | ||
| } | ||
| /** | ||
| * Get the logger | ||
| * @returns {ILogger | undefined} The logger instance | ||
| */ | ||
| get logger() { | ||
| return this._logger; | ||
| } | ||
| /** | ||
| * Set the logger | ||
| * @param {ILogger | undefined} logger - The logger to set | ||
| */ | ||
| set logger(logger) { | ||
| this._logger = logger; | ||
| } | ||
| /** | ||
| * Use the hash to check if the file has changed | ||
| * @returns {boolean} if the hash is used to check if the file has changed (default: false) | ||
| */ | ||
| get useCheckSum() { | ||
| return this._useCheckSum; | ||
| } | ||
| /** | ||
| * Set the useCheckSum value | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set useCheckSum(value) { | ||
| this._useCheckSum = value; | ||
| } | ||
| /** | ||
| * Get the hash algorithm | ||
| * @returns {string} The hash algorithm (default: 'md5') | ||
| */ | ||
| get hashAlgorithm() { | ||
| return this._hashAlgorithm; | ||
| } | ||
| /** | ||
| * Set the hash algorithm | ||
| * @param {string} value - The value to set | ||
| */ | ||
| set hashAlgorithm(value) { | ||
| this._hashAlgorithm = value; | ||
| } | ||
| /** | ||
| * Get the current working directory | ||
| * @returns {string} The current working directory (default: process.cwd()) | ||
| */ | ||
| get cwd() { | ||
| return this._cwd; | ||
| } | ||
| /** | ||
| * Set the current working directory | ||
| * @param {string} value - The value to set | ||
| */ | ||
| set cwd(value) { | ||
| this._cwd = value; | ||
| } | ||
| /** | ||
| * Get whether to use modified time for change detection | ||
| * @returns {boolean} Whether modified time (mtime) is used for change detection (default: true) | ||
| */ | ||
| get useModifiedTime() { | ||
| return this._useModifiedTime; | ||
| } | ||
| /** | ||
| * Set whether to use modified time for change detection | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set useModifiedTime(value) { | ||
| this._useModifiedTime = value; | ||
| } | ||
| /** | ||
| * Get whether to restrict paths to cwd boundaries | ||
| * @returns {boolean} Whether strict path checking is enabled (default: true) | ||
| */ | ||
| get restrictAccessToCwd() { | ||
| return this._restrictAccessToCwd; | ||
| } | ||
| /** | ||
| * Set whether to restrict paths to cwd boundaries | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set restrictAccessToCwd(value) { | ||
| this._restrictAccessToCwd = value; | ||
| } | ||
| /** | ||
| * Get whether to use absolute path as cache key | ||
| * @returns {boolean} Whether cache keys use absolute paths (default: false) | ||
| */ | ||
| get useAbsolutePathAsKey() { | ||
| return this._useAbsolutePathAsKey; | ||
| } | ||
| /** | ||
| * Set whether to use absolute path as cache key | ||
| * @param {boolean} value - The value to set | ||
| */ | ||
| set useAbsolutePathAsKey(value) { | ||
| this._useAbsolutePathAsKey = value; | ||
| } | ||
| /** | ||
| * Given a buffer, calculate md5 hash of its content. | ||
| * @method getHash | ||
| * @param {Buffer} buffer buffer to calculate hash on | ||
| * @return {String} content hash digest | ||
| */ | ||
| getHash(buffer) { | ||
| return crypto.createHash(this._hashAlgorithm).update(buffer).digest("hex"); | ||
| } | ||
| /** | ||
| * Create the key for the file path used for caching. | ||
| * @method createFileKey | ||
| * @param {String} filePath | ||
| * @return {String} | ||
| */ | ||
| createFileKey(filePath) { | ||
| let result = filePath; | ||
| if (this._useAbsolutePathAsKey && this.isRelativePath(filePath)) { | ||
| result = this.getAbsolutePathWithCwd(filePath, this._cwd); | ||
| } | ||
| return result; | ||
| } | ||
| /** | ||
| * Check if the file path is a relative path | ||
| * @method isRelativePath | ||
| * @param filePath - The file path to check | ||
| * @returns {boolean} if the file path is a relative path, false otherwise | ||
| */ | ||
| isRelativePath(filePath) { | ||
| return !path.isAbsolute(filePath); | ||
| } | ||
| /** | ||
| * Delete the cache file from the disk | ||
| * @method deleteCacheFile | ||
| * @return {boolean} true if the file was deleted, false otherwise | ||
| */ | ||
| deleteCacheFile() { | ||
| return this._cache.removeCacheFile(); | ||
| } | ||
| /** | ||
| * Remove the cache from the file and clear the memory cache | ||
| * @method destroy | ||
| */ | ||
| destroy() { | ||
| this._cache.destroy(); | ||
| } | ||
| /** | ||
| * Remove and Entry From the Cache | ||
| * @method removeEntry | ||
| * @param filePath - The file path to remove from the cache | ||
| */ | ||
| removeEntry(filePath) { | ||
| const key = this.createFileKey(filePath); | ||
| this._cache.removeKey(key); | ||
| } | ||
| /** | ||
| * Reconcile the cache | ||
| * @method reconcile | ||
| */ | ||
| reconcile() { | ||
| const { items } = this._cache; | ||
| for (const item of items) { | ||
| const fileDescriptor = this.getFileDescriptor(item.key); | ||
| if (fileDescriptor.notFound) { | ||
| this._cache.removeKey(item.key); | ||
| } | ||
| } | ||
| this._cache.save(); | ||
| } | ||
| /** | ||
| * Check if the file has changed | ||
| * @method hasFileChanged | ||
| * @param filePath - The file path to check | ||
| * @returns {boolean} if the file has changed, false otherwise | ||
| */ | ||
| hasFileChanged(filePath) { | ||
| let result = false; | ||
| const fileDescriptor = this.getFileDescriptor(filePath); | ||
| if ((!fileDescriptor.err || !fileDescriptor.notFound) && fileDescriptor.changed) { | ||
| result = true; | ||
| } | ||
| return result; | ||
| } | ||
| /** | ||
| * Get the file descriptor for the file path | ||
| * @method getFileDescriptor | ||
| * @param filePath - The file path to get the file descriptor for | ||
| * @param options - The options for getting the file descriptor | ||
| * @returns The file descriptor | ||
| */ | ||
| getFileDescriptor(filePath, options) { | ||
| this._logger?.debug({ filePath, options }, "Getting file descriptor"); | ||
| let fstat; | ||
| const result = { | ||
| key: this.createFileKey(filePath), | ||
| changed: false, | ||
| meta: {} | ||
| }; | ||
| this._logger?.trace({ key: result.key }, "Created file key"); | ||
| const metaCache = this._cache.getKey(result.key); | ||
| if (metaCache) { | ||
| this._logger?.trace({ metaCache }, "Found cached meta"); | ||
| } else { | ||
| this._logger?.trace("No cached meta found"); | ||
| } | ||
| result.meta = metaCache ? { ...metaCache } : {}; | ||
| const absolutePath = this.getAbsolutePath(filePath); | ||
| this._logger?.trace({ absolutePath }, "Resolved absolute path"); | ||
| const useCheckSumValue = options?.useCheckSum ?? this._useCheckSum; | ||
| this._logger?.debug( | ||
| { useCheckSum: useCheckSumValue }, | ||
| "Using checksum setting" | ||
| ); | ||
| const useModifiedTimeValue = options?.useModifiedTime ?? this.useModifiedTime; | ||
| this._logger?.debug( | ||
| { useModifiedTime: useModifiedTimeValue }, | ||
| "Using modified time (mtime) setting" | ||
| ); | ||
| try { | ||
| fstat = fs.statSync(absolutePath); | ||
| result.meta.size = fstat.size; | ||
| result.meta.mtime = fstat.mtime.getTime(); | ||
| this._logger?.trace( | ||
| { size: result.meta.size, mtime: result.meta.mtime }, | ||
| "Read file stats" | ||
| ); | ||
| if (useCheckSumValue) { | ||
| const buffer = fs.readFileSync(absolutePath); | ||
| result.meta.hash = this.getHash(buffer); | ||
| this._logger?.trace({ hash: result.meta.hash }, "Calculated file hash"); | ||
| } | ||
| } catch (error) { | ||
| this._logger?.error({ filePath, error }, "Error reading file"); | ||
| this.removeEntry(filePath); | ||
| let notFound = false; | ||
| if (error.message.includes("ENOENT")) { | ||
| notFound = true; | ||
| this._logger?.debug({ filePath }, "File not found"); | ||
| } | ||
| return { | ||
| key: result.key, | ||
| err: error, | ||
| notFound, | ||
| meta: {} | ||
| }; | ||
| } | ||
| if (!metaCache) { | ||
| result.changed = true; | ||
| this._cache.setKey(result.key, result.meta); | ||
| this._logger?.debug({ filePath }, "File not in cache, marked as changed"); | ||
| return result; | ||
| } | ||
| if (useModifiedTimeValue && metaCache?.mtime !== result.meta?.mtime) { | ||
| result.changed = true; | ||
| this._logger?.debug( | ||
| { filePath, oldMtime: metaCache.mtime, newMtime: result.meta.mtime }, | ||
| "File changed: mtime differs" | ||
| ); | ||
| } | ||
| if (metaCache?.size !== result.meta?.size) { | ||
| result.changed = true; | ||
| this._logger?.debug( | ||
| { filePath, oldSize: metaCache.size, newSize: result.meta.size }, | ||
| "File changed: size differs" | ||
| ); | ||
| } | ||
| if (useCheckSumValue && metaCache?.hash !== result.meta?.hash) { | ||
| result.changed = true; | ||
| this._logger?.debug( | ||
| { filePath, oldHash: metaCache.hash, newHash: result.meta.hash }, | ||
| "File changed: hash differs" | ||
| ); | ||
| } | ||
| this._cache.setKey(result.key, result.meta); | ||
| if (result.changed) { | ||
| this._logger?.info({ filePath }, "File has changed"); | ||
| } else { | ||
| this._logger?.debug({ filePath }, "File unchanged"); | ||
| } | ||
| return result; | ||
| } | ||
| /** | ||
| * Get the file descriptors for the files | ||
| * @method normalizeEntries | ||
| * @param files?: string[] - The files to get the file descriptors for | ||
| * @returns The file descriptors | ||
| */ | ||
| normalizeEntries(files) { | ||
| const result = []; | ||
| if (files) { | ||
| for (const file of files) { | ||
| const fileDescriptor = this.getFileDescriptor(file); | ||
| result.push(fileDescriptor); | ||
| } | ||
| return result; | ||
| } | ||
| const keys = this.cache.keys(); | ||
| for (const key of keys) { | ||
| const fileDescriptor = this.getFileDescriptor(key); | ||
| if (!fileDescriptor.notFound && !fileDescriptor.err) { | ||
| result.push(fileDescriptor); | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| /** | ||
| * Analyze the files | ||
| * @method analyzeFiles | ||
| * @param files - The files to analyze | ||
| * @returns {AnalyzedFiles} The analysis of the files | ||
| */ | ||
| analyzeFiles(files) { | ||
| const result = { | ||
| changedFiles: [], | ||
| notFoundFiles: [], | ||
| notChangedFiles: [] | ||
| }; | ||
| const fileDescriptors = this.normalizeEntries(files); | ||
| for (const fileDescriptor of fileDescriptors) { | ||
| if (fileDescriptor.notFound) { | ||
| result.notFoundFiles.push(fileDescriptor.key); | ||
| } else if (fileDescriptor.changed) { | ||
| result.changedFiles.push(fileDescriptor.key); | ||
| } else { | ||
| result.notChangedFiles.push(fileDescriptor.key); | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| /** | ||
| * Get the updated files | ||
| * @method getUpdatedFiles | ||
| * @param files - The files to get the updated files for | ||
| * @returns {string[]} The updated files | ||
| */ | ||
| getUpdatedFiles(files) { | ||
| const result = []; | ||
| const fileDescriptors = this.normalizeEntries(files); | ||
| for (const fileDescriptor of fileDescriptors) { | ||
| if (fileDescriptor.changed) { | ||
| result.push(fileDescriptor.key); | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| /** | ||
| * Get the file descriptors by path prefix | ||
| * @method getFileDescriptorsByPath | ||
| * @param filePath - the path prefix to match | ||
| * @returns {FileDescriptor[]} The file descriptors | ||
| */ | ||
| getFileDescriptorsByPath(filePath) { | ||
| const result = []; | ||
| const keys = this._cache.keys(); | ||
| for (const key of keys) { | ||
| if (key.startsWith(filePath)) { | ||
| const fileDescriptor = this.getFileDescriptor(key); | ||
| result.push(fileDescriptor); | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| /** | ||
| * Get the Absolute Path. If it is already absolute it will return the path as is. | ||
| * When restrictAccessToCwd is enabled, ensures the resolved path stays within cwd boundaries. | ||
| * @method getAbsolutePath | ||
| * @param filePath - The file path to get the absolute path for | ||
| * @returns {string} | ||
| * @throws {Error} When restrictAccessToCwd is true and path would resolve outside cwd | ||
| */ | ||
| getAbsolutePath(filePath) { | ||
| if (this.isRelativePath(filePath)) { | ||
| const sanitizedPath = filePath.replace(/\0/g, ""); | ||
| const resolved = path.resolve(this._cwd, sanitizedPath); | ||
| if (this._restrictAccessToCwd) { | ||
| const normalizedResolved = path.normalize(resolved); | ||
| const normalizedCwd = path.normalize(this._cwd); | ||
| const isWithinCwd = normalizedResolved === normalizedCwd || normalizedResolved.startsWith(normalizedCwd + path.sep); | ||
| if (!isWithinCwd) { | ||
| throw new Error( | ||
| `Path traversal attempt blocked: "${filePath}" resolves outside of working directory "${this._cwd}"` | ||
| ); | ||
| } | ||
| } | ||
| return resolved; | ||
| } | ||
| return filePath; | ||
| } | ||
| /** | ||
| * Get the Absolute Path with a custom working directory. If it is already absolute it will return the path as is. | ||
| * When restrictAccessToCwd is enabled, ensures the resolved path stays within the provided cwd boundaries. | ||
| * @method getAbsolutePathWithCwd | ||
| * @param filePath - The file path to get the absolute path for | ||
| * @param cwd - The custom working directory to resolve relative paths from | ||
| * @returns {string} | ||
| * @throws {Error} When restrictAccessToCwd is true and path would resolve outside the provided cwd | ||
| */ | ||
| getAbsolutePathWithCwd(filePath, cwd) { | ||
| if (this.isRelativePath(filePath)) { | ||
| const sanitizedPath = filePath.replace(/\0/g, ""); | ||
| const resolved = path.resolve(cwd, sanitizedPath); | ||
| if (this._restrictAccessToCwd) { | ||
| const normalizedResolved = path.normalize(resolved); | ||
| const normalizedCwd = path.normalize(cwd); | ||
| const isWithinCwd = normalizedResolved === normalizedCwd || normalizedResolved.startsWith(normalizedCwd + path.sep); | ||
| if (!isWithinCwd) { | ||
| throw new Error( | ||
| `Path traversal attempt blocked: "${filePath}" resolves outside of working directory "${cwd}"` | ||
| ); | ||
| } | ||
| } | ||
| return resolved; | ||
| } | ||
| return filePath; | ||
| } | ||
| /** | ||
| * Rename cache keys that start with a given path prefix. | ||
| * @method renameCacheKeys | ||
| * @param oldPath - The old path prefix to rename | ||
| * @param newPath - The new path prefix to rename to | ||
| */ | ||
| renameCacheKeys(oldPath, newPath) { | ||
| const keys = this._cache.keys(); | ||
| for (const key of keys) { | ||
| if (key.startsWith(oldPath)) { | ||
| const newKey = key.replace(oldPath, newPath); | ||
| const meta = this._cache.getKey(key); | ||
| this._cache.removeKey(key); | ||
| this._cache.setKey(newKey, meta); | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| export { | ||
| FileEntryCache, | ||
| create, | ||
| createFromFile, | ||
| FileEntryDefault as default | ||
| }; | ||
| /* v8 ignore next -- @preserve */ |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
89300
2.38%634
0.63%0
-100%1070
-22.74%1
Infinity%Updated