🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

file-entry-cache

Package Overview
Dependencies
Maintainers
1
Versions
44
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

file-entry-cache - npm Package Compare versions

Comparing version
11.1.3
to
11.1.5
+290
dist/index.d.mts
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 };
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;

@@ -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 };
{
"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",

@@ -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

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 };
// 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 */