🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
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.0.0-beta.3
to
11.0.0-beta.4
+1
-464
dist/index.cjs

@@ -1,464 +0,1 @@

"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
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;
};
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");
function createFromFile(filePath, useCheckSum, cwd) {
const fname = import_node_path.default.basename(filePath);
const directory = import_node_path.default.dirname(filePath);
return create(fname, directory, useCheckSum, cwd);
}
function create(cacheId, cacheDirectory, useCheckSum, cwd) {
const options = {
useCheckSum,
cwd,
cache: {
cacheId,
cacheDir: cacheDirectory
}
};
const fileEntryCache = new FileEntryCache(options);
if (cacheDirectory) {
const cachePath = `${cacheDirectory}/${cacheId}`;
if (import_node_fs.default.existsSync(cachePath)) {
fileEntryCache.cache = (0, import_flat_cache.createFromFile)(cachePath, options.cache);
}
}
return fileEntryCache;
}
var FileEntryDefault = class {
static create = create;
static createFromFile = createFromFile;
};
var FileEntryCache = class {
_cache = new import_flat_cache.FlatCache({ useClone: false });
_useCheckSum = false;
_hashAlgorithm = "md5";
_cwd = process.cwd();
_strictPaths = 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?.strictPaths !== void 0) {
this._strictPaths = options.strictPaths;
}
}
/**
* 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;
}
/**
* 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 restrict paths to cwd boundaries
* @returns {boolean} Whether strict path checking is enabled (default: true)
*/
get strictPaths() {
return this._strictPaths;
}
/**
* Set whether to restrict paths to cwd boundaries
* @param {boolean} value - The value to set
*/
set strictPaths(value) {
this._strictPaths = 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) {
return filePath;
}
/**
* 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) {
let fstat;
const result = {
key: this.createFileKey(filePath),
changed: false,
meta: {}
};
result.meta = this._cache.getKey(result.key) ?? {};
const absolutePath = this.getAbsolutePath(filePath);
const useCheckSumValue = options?.useCheckSum ?? this._useCheckSum;
try {
fstat = import_node_fs.default.statSync(absolutePath);
result.meta = {
size: fstat.size
};
result.meta.mtime = fstat.mtime.getTime();
if (useCheckSumValue) {
const buffer = import_node_fs.default.readFileSync(absolutePath);
result.meta.hash = this.getHash(buffer);
}
} catch (error) {
this.removeEntry(filePath);
let notFound = false;
if (error.message.includes("ENOENT")) {
notFound = true;
}
return {
key: result.key,
err: error,
notFound,
meta: {}
};
}
const metaCache = this._cache.getKey(result.key);
if (!metaCache) {
result.changed = true;
this._cache.setKey(result.key, result.meta);
return result;
}
if (result.meta.data === void 0) {
result.meta.data = metaCache.data;
}
if (useCheckSumValue === false && metaCache?.mtime !== result.meta?.mtime) {
result.changed = true;
}
if (metaCache?.size !== result.meta?.size) {
result.changed = true;
}
if (useCheckSumValue && metaCache?.hash !== result.meta?.hash) {
result.changed = true;
}
this._cache.setKey(result.key, result.meta);
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 strictPaths 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 strictPaths 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._strictPaths) {
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 strictPaths 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 strictPaths 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._strictPaths) {
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);
}
}
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
FileEntryCache,
create,
createFromFile
});
"use strict";var k=Object.create;var g=Object.defineProperty;var C=Object.getOwnPropertyDescriptor;var w=Object.getOwnPropertyNames;var v=Object.getPrototypeOf,z=Object.prototype.hasOwnProperty;var D=(o,e)=>{for(var s in e)g(o,s,{get:e[s],enumerable:!0})},b=(o,e,s,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let t of w(e))!z.call(o,t)&&t!==s&&g(o,t,{get:()=>e[t],enumerable:!(i=C(e,t))||i.enumerable});return o};var p=(o,e,s)=>(s=o!=null?k(v(o)):{},b(e||!o||!o.__esModule?g(s,"default",{value:o,enumerable:!0}):s,o)),S=o=>b(g({},"__esModule",{value:!0}),o);var A={};D(A,{FileEntryCache:()=>m,create:()=>f,createFromFile:()=>_,default:()=>d});module.exports=S(A);var y=p(require("crypto"),1),u=p(require("fs"),1),n=p(require("path"),1),l=require("flat-cache");function _(o,e,s){let i=n.default.basename(o),t=n.default.dirname(o);return f(i,t,e,s)}function f(o,e,s,i){let t={useCheckSum:s,cwd:i,cache:{cacheId:o,cacheDir:e}},r=new m(t);if(e){let c=`${e}/${o}`;u.default.existsSync(c)&&(r.cache=(0,l.createFromFile)(c,t.cache))}return r}var d=class{static create=f;static createFromFile=_},m=class{_cache=new l.FlatCache({useClone:!1});_useCheckSum=!1;_hashAlgorithm="md5";_cwd=process.cwd();_strictPaths=!0;_logger;constructor(e){e?.cache&&(this._cache=new l.FlatCache(e.cache)),e?.useCheckSum&&(this._useCheckSum=e.useCheckSum),e?.hashAlgorithm&&(this._hashAlgorithm=e.hashAlgorithm),e?.cwd&&(this._cwd=e.cwd),e?.strictPaths!==void 0&&(this._strictPaths=e.strictPaths),e?.logger&&(this._logger=e.logger)}get cache(){return this._cache}set cache(e){this._cache=e}get logger(){return this._logger}set logger(e){this._logger=e}get useCheckSum(){return this._useCheckSum}set useCheckSum(e){this._useCheckSum=e}get hashAlgorithm(){return this._hashAlgorithm}set hashAlgorithm(e){this._hashAlgorithm=e}get cwd(){return this._cwd}set cwd(e){this._cwd=e}get strictPaths(){return this._strictPaths}set strictPaths(e){this._strictPaths=e}getHash(e){return y.default.createHash(this._hashAlgorithm).update(e).digest("hex")}createFileKey(e){return e}isRelativePath(e){return!n.default.isAbsolute(e)}deleteCacheFile(){return this._cache.removeCacheFile()}destroy(){this._cache.destroy()}removeEntry(e){let s=this.createFileKey(e);this._cache.removeKey(s)}reconcile(){let{items:e}=this._cache;for(let s of e)this.getFileDescriptor(s.key).notFound&&this._cache.removeKey(s.key);this._cache.save()}hasFileChanged(e){let s=!1,i=this.getFileDescriptor(e);return(!i.err||!i.notFound)&&i.changed&&(s=!0),s}getFileDescriptor(e,s){this._logger?.debug({filePath:e,options:s},"Getting file descriptor");let i,t={key:this.createFileKey(e),changed:!1,meta:{}};this._logger?.trace({key:t.key},"Created file key");let r=this._cache.getKey(t.key);r?this._logger?.trace({metaCache:r},"Found cached meta"):this._logger?.trace("No cached meta found"),t.meta=r?{...r}:{};let c=this.getAbsolutePath(e);this._logger?.trace({absolutePath:c},"Resolved absolute path");let a=s?.useCheckSum??this._useCheckSum;this._logger?.debug({useCheckSum:a},"Using checksum setting");try{if(i=u.default.statSync(c),t.meta.size=i.size,t.meta.mtime=i.mtime.getTime(),this._logger?.trace({size:t.meta.size,mtime:t.meta.mtime},"Read file stats"),a){let h=u.default.readFileSync(c);t.meta.hash=this.getHash(h),this._logger?.trace({hash:t.meta.hash},"Calculated file hash")}}catch(h){this._logger?.error({filePath:e,error:h},"Error reading file"),this.removeEntry(e);let F=!1;return h.message.includes("ENOENT")&&(F=!0,this._logger?.debug({filePath:e},"File not found")),{key:t.key,err:h,notFound:F,meta:{}}}return r?(a===!1&&r?.mtime!==t.meta?.mtime&&(t.changed=!0,this._logger?.debug({filePath:e,oldMtime:r.mtime,newMtime:t.meta.mtime},"File changed: mtime differs")),r?.size!==t.meta?.size&&(t.changed=!0,this._logger?.debug({filePath:e,oldSize:r.size,newSize:t.meta.size},"File changed: size differs")),a&&r?.hash!==t.meta?.hash&&(t.changed=!0,this._logger?.debug({filePath:e,oldHash:r.hash,newHash:t.meta.hash},"File changed: hash differs")),this._cache.setKey(t.key,t.meta),t.changed?this._logger?.info({filePath:e},"File has changed"):this._logger?.debug({filePath:e},"File unchanged"),t):(t.changed=!0,this._cache.setKey(t.key,t.meta),this._logger?.debug({filePath:e},"File not in cache, marked as changed"),t)}normalizeEntries(e){let s=[];if(e){for(let t of e){let r=this.getFileDescriptor(t);s.push(r)}return s}let i=this.cache.keys();for(let t of i){let r=this.getFileDescriptor(t);!r.notFound&&!r.err&&s.push(r)}return s}analyzeFiles(e){let s={changedFiles:[],notFoundFiles:[],notChangedFiles:[]},i=this.normalizeEntries(e);for(let t of i)t.notFound?s.notFoundFiles.push(t.key):t.changed?s.changedFiles.push(t.key):s.notChangedFiles.push(t.key);return s}getUpdatedFiles(e){let s=[],i=this.normalizeEntries(e);for(let t of i)t.changed&&s.push(t.key);return s}getFileDescriptorsByPath(e){let s=[],i=this._cache.keys();for(let t of i)if(t.startsWith(e)){let r=this.getFileDescriptor(t);s.push(r)}return s}getAbsolutePath(e){if(this.isRelativePath(e)){let s=e.replace(/\0/g,""),i=n.default.resolve(this._cwd,s);if(this._strictPaths){let t=n.default.normalize(i),r=n.default.normalize(this._cwd);if(!(t===r||t.startsWith(r+n.default.sep)))throw new Error(`Path traversal attempt blocked: "${e}" resolves outside of working directory "${this._cwd}"`)}return i}return e}getAbsolutePathWithCwd(e,s){if(this.isRelativePath(e)){let i=e.replace(/\0/g,""),t=n.default.resolve(s,i);if(this._strictPaths){let r=n.default.normalize(t),c=n.default.normalize(s);if(!(r===c||r.startsWith(c+n.default.sep)))throw new Error(`Path traversal attempt blocked: "${e}" resolves outside of working directory "${s}"`)}return t}return e}renameCacheKeys(e,s){let i=this._cache.keys();for(let t of i)if(t.startsWith(e)){let r=t.replace(e,s),c=this._cache.getKey(t);this._cache.removeKey(t),this._cache.setKey(r,c)}}};0&&(module.exports={FileEntryCache,create,createFromFile});
+32
-1
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 = {

@@ -15,2 +31,4 @@ /** Whether to use file modified time for change detection (default: true) */

strictPaths?: boolean;
/** Logger instance for logging (default: undefined) */
logger?: ILogger;
/** Options for the underlying flat cache */

@@ -44,2 +62,4 @@ cache?: FlatCacheOptions;

data?: unknown;
/** Allow any additional custom properties */
[key: string]: unknown;
};

@@ -81,2 +101,3 @@ type AnalyzedFiles = {

private _strictPaths;
private _logger?;
/**

@@ -98,2 +119,12 @@ * Create a new FileEntryCache instance

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

@@ -252,2 +283,2 @@ * @returns {boolean} if the hash is used to check if the file has changed (default: false)

export { type AnalyzedFiles, type FileDescriptor, type FileDescriptorMeta, FileEntryCache, type FileEntryCacheOptions, type GetFileDescriptorOptions, create, createFromFile, FileEntryDefault as default };
export { type AnalyzedFiles, type FileDescriptor, type FileDescriptorMeta, FileEntryCache, type FileEntryCacheOptions, type GetFileDescriptorOptions, type ILogger, create, createFromFile, FileEntryDefault as default };
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 = {

@@ -15,2 +31,4 @@ /** Whether to use file modified time for change detection (default: true) */

strictPaths?: boolean;
/** Logger instance for logging (default: undefined) */
logger?: ILogger;
/** Options for the underlying flat cache */

@@ -44,2 +62,4 @@ cache?: FlatCacheOptions;

data?: unknown;
/** Allow any additional custom properties */
[key: string]: unknown;
};

@@ -81,2 +101,3 @@ type AnalyzedFiles = {

private _strictPaths;
private _logger?;
/**

@@ -98,2 +119,12 @@ * Create a new FileEntryCache instance

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

@@ -252,2 +283,2 @@ * @returns {boolean} if the hash is used to check if the file has changed (default: false)

export { type AnalyzedFiles, type FileDescriptor, type FileDescriptorMeta, FileEntryCache, type FileEntryCacheOptions, type GetFileDescriptorOptions, create, createFromFile, FileEntryDefault as default };
export { type AnalyzedFiles, type FileDescriptor, type FileDescriptorMeta, FileEntryCache, type FileEntryCacheOptions, type GetFileDescriptorOptions, type ILogger, create, createFromFile, FileEntryDefault as default };

@@ -1,430 +0,1 @@

// 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, useCheckSum, cwd) {
const fname = path.basename(filePath);
const directory = path.dirname(filePath);
return create(fname, directory, useCheckSum, cwd);
}
function create(cacheId, cacheDirectory, useCheckSum, cwd) {
const options = {
useCheckSum,
cwd,
cache: {
cacheId,
cacheDir: cacheDirectory
}
};
const fileEntryCache = new FileEntryCache(options);
if (cacheDirectory) {
const cachePath = `${cacheDirectory}/${cacheId}`;
if (fs.existsSync(cachePath)) {
fileEntryCache.cache = createFlatCacheFile(cachePath, options.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();
_strictPaths = 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?.strictPaths !== void 0) {
this._strictPaths = options.strictPaths;
}
}
/**
* 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;
}
/**
* 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 restrict paths to cwd boundaries
* @returns {boolean} Whether strict path checking is enabled (default: true)
*/
get strictPaths() {
return this._strictPaths;
}
/**
* Set whether to restrict paths to cwd boundaries
* @param {boolean} value - The value to set
*/
set strictPaths(value) {
this._strictPaths = 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) {
return filePath;
}
/**
* 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) {
let fstat;
const result = {
key: this.createFileKey(filePath),
changed: false,
meta: {}
};
result.meta = this._cache.getKey(result.key) ?? {};
const absolutePath = this.getAbsolutePath(filePath);
const useCheckSumValue = options?.useCheckSum ?? this._useCheckSum;
try {
fstat = fs.statSync(absolutePath);
result.meta = {
size: fstat.size
};
result.meta.mtime = fstat.mtime.getTime();
if (useCheckSumValue) {
const buffer = fs.readFileSync(absolutePath);
result.meta.hash = this.getHash(buffer);
}
} catch (error) {
this.removeEntry(filePath);
let notFound = false;
if (error.message.includes("ENOENT")) {
notFound = true;
}
return {
key: result.key,
err: error,
notFound,
meta: {}
};
}
const metaCache = this._cache.getKey(result.key);
if (!metaCache) {
result.changed = true;
this._cache.setKey(result.key, result.meta);
return result;
}
if (result.meta.data === void 0) {
result.meta.data = metaCache.data;
}
if (useCheckSumValue === false && metaCache?.mtime !== result.meta?.mtime) {
result.changed = true;
}
if (metaCache?.size !== result.meta?.size) {
result.changed = true;
}
if (useCheckSumValue && metaCache?.hash !== result.meta?.hash) {
result.changed = true;
}
this._cache.setKey(result.key, result.meta);
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 strictPaths 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 strictPaths 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._strictPaths) {
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 strictPaths 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 strictPaths 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._strictPaths) {
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
};
import f from"crypto";import l from"fs";import c from"path";import{createFromFile as F,FlatCache as m}from"flat-cache";function b(n,e,s){let r=c.basename(n),t=c.dirname(n);return p(r,t,e,s)}function p(n,e,s,r){let t={useCheckSum:s,cwd:r,cache:{cacheId:n,cacheDir:e}},i=new u(t);if(e){let o=`${e}/${n}`;l.existsSync(o)&&(i.cache=F(o,t.cache))}return i}var g=class{static create=p;static createFromFile=b},u=class{_cache=new m({useClone:!1});_useCheckSum=!1;_hashAlgorithm="md5";_cwd=process.cwd();_strictPaths=!0;_logger;constructor(e){e?.cache&&(this._cache=new m(e.cache)),e?.useCheckSum&&(this._useCheckSum=e.useCheckSum),e?.hashAlgorithm&&(this._hashAlgorithm=e.hashAlgorithm),e?.cwd&&(this._cwd=e.cwd),e?.strictPaths!==void 0&&(this._strictPaths=e.strictPaths),e?.logger&&(this._logger=e.logger)}get cache(){return this._cache}set cache(e){this._cache=e}get logger(){return this._logger}set logger(e){this._logger=e}get useCheckSum(){return this._useCheckSum}set useCheckSum(e){this._useCheckSum=e}get hashAlgorithm(){return this._hashAlgorithm}set hashAlgorithm(e){this._hashAlgorithm=e}get cwd(){return this._cwd}set cwd(e){this._cwd=e}get strictPaths(){return this._strictPaths}set strictPaths(e){this._strictPaths=e}getHash(e){return f.createHash(this._hashAlgorithm).update(e).digest("hex")}createFileKey(e){return e}isRelativePath(e){return!c.isAbsolute(e)}deleteCacheFile(){return this._cache.removeCacheFile()}destroy(){this._cache.destroy()}removeEntry(e){let s=this.createFileKey(e);this._cache.removeKey(s)}reconcile(){let{items:e}=this._cache;for(let s of e)this.getFileDescriptor(s.key).notFound&&this._cache.removeKey(s.key);this._cache.save()}hasFileChanged(e){let s=!1,r=this.getFileDescriptor(e);return(!r.err||!r.notFound)&&r.changed&&(s=!0),s}getFileDescriptor(e,s){this._logger?.debug({filePath:e,options:s},"Getting file descriptor");let r,t={key:this.createFileKey(e),changed:!1,meta:{}};this._logger?.trace({key:t.key},"Created file key");let i=this._cache.getKey(t.key);i?this._logger?.trace({metaCache:i},"Found cached meta"):this._logger?.trace("No cached meta found"),t.meta=i?{...i}:{};let o=this.getAbsolutePath(e);this._logger?.trace({absolutePath:o},"Resolved absolute path");let a=s?.useCheckSum??this._useCheckSum;this._logger?.debug({useCheckSum:a},"Using checksum setting");try{if(r=l.statSync(o),t.meta.size=r.size,t.meta.mtime=r.mtime.getTime(),this._logger?.trace({size:t.meta.size,mtime:t.meta.mtime},"Read file stats"),a){let h=l.readFileSync(o);t.meta.hash=this.getHash(h),this._logger?.trace({hash:t.meta.hash},"Calculated file hash")}}catch(h){this._logger?.error({filePath:e,error:h},"Error reading file"),this.removeEntry(e);let d=!1;return h.message.includes("ENOENT")&&(d=!0,this._logger?.debug({filePath:e},"File not found")),{key:t.key,err:h,notFound:d,meta:{}}}return i?(a===!1&&i?.mtime!==t.meta?.mtime&&(t.changed=!0,this._logger?.debug({filePath:e,oldMtime:i.mtime,newMtime:t.meta.mtime},"File changed: mtime differs")),i?.size!==t.meta?.size&&(t.changed=!0,this._logger?.debug({filePath:e,oldSize:i.size,newSize:t.meta.size},"File changed: size differs")),a&&i?.hash!==t.meta?.hash&&(t.changed=!0,this._logger?.debug({filePath:e,oldHash:i.hash,newHash:t.meta.hash},"File changed: hash differs")),this._cache.setKey(t.key,t.meta),t.changed?this._logger?.info({filePath:e},"File has changed"):this._logger?.debug({filePath:e},"File unchanged"),t):(t.changed=!0,this._cache.setKey(t.key,t.meta),this._logger?.debug({filePath:e},"File not in cache, marked as changed"),t)}normalizeEntries(e){let s=[];if(e){for(let t of e){let i=this.getFileDescriptor(t);s.push(i)}return s}let r=this.cache.keys();for(let t of r){let i=this.getFileDescriptor(t);!i.notFound&&!i.err&&s.push(i)}return s}analyzeFiles(e){let s={changedFiles:[],notFoundFiles:[],notChangedFiles:[]},r=this.normalizeEntries(e);for(let t of r)t.notFound?s.notFoundFiles.push(t.key):t.changed?s.changedFiles.push(t.key):s.notChangedFiles.push(t.key);return s}getUpdatedFiles(e){let s=[],r=this.normalizeEntries(e);for(let t of r)t.changed&&s.push(t.key);return s}getFileDescriptorsByPath(e){let s=[],r=this._cache.keys();for(let t of r)if(t.startsWith(e)){let i=this.getFileDescriptor(t);s.push(i)}return s}getAbsolutePath(e){if(this.isRelativePath(e)){let s=e.replace(/\0/g,""),r=c.resolve(this._cwd,s);if(this._strictPaths){let t=c.normalize(r),i=c.normalize(this._cwd);if(!(t===i||t.startsWith(i+c.sep)))throw new Error(`Path traversal attempt blocked: "${e}" resolves outside of working directory "${this._cwd}"`)}return r}return e}getAbsolutePathWithCwd(e,s){if(this.isRelativePath(e)){let r=e.replace(/\0/g,""),t=c.resolve(s,r);if(this._strictPaths){let i=c.normalize(t),o=c.normalize(s);if(!(i===o||i.startsWith(o+c.sep)))throw new Error(`Path traversal attempt blocked: "${e}" resolves outside of working directory "${s}"`)}return t}return e}renameCacheKeys(e,s){let r=this._cache.keys();for(let t of r)if(t.startsWith(e)){let i=t.replace(e,s),o=this._cache.getKey(t);this._cache.removeKey(t),this._cache.setKey(i,o)}}};export{u as FileEntryCache,p as create,b as createFromFile,g as default};
{
"name": "file-entry-cache",
"version": "11.0.0-beta.3",
"version": "11.0.0-beta.4",
"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",

@@ -33,4 +33,5 @@ "type": "module",

"@biomejs/biome": "^2.2.5",
"@types/node": "^24.6.2",
"@types/node": "^24.7.0",
"@vitest/coverage-v8": "^3.2.4",
"pino": "^10.0.0",
"rimraf": "^6.0.1",

@@ -49,3 +50,3 @@ "tsup": "^8.5.0",

"scripts": {
"build": "rimraf ./dist && tsup src/index.ts --format cjs,esm --dts --clean",
"build": "rimraf ./dist && tsup src/index.ts --format cjs,esm --dts --clean --minify",
"prepublish": "pnpm build",

@@ -52,0 +53,0 @@ "lint": "biome check --write --error-on-warnings",

+129
-2

@@ -39,2 +39,8 @@ [<img align="center" src="https://cacheable.org/symbol.svg" alt="Cacheable" />](https://github.com/jaredwray/cacheable)

- [Setting Additional Meta Data](#setting-additional-meta-data)
- [Logger Support](#logger-support)
- [Logger Interface](#logger-interface)
- [Using Pino Logger](#using-pino-logger)
- [Log Levels](#log-levels)
- [Example with Custom Log Levels](#example-with-custom-log-levels)
- [Debugging Cache Operations](#debugging-cache-operations)
- [How to Contribute](#how-to-contribute)

@@ -121,2 +127,3 @@ - [License and Copyright](#license-and-copyright)

- `strictPaths?` - If `true` restricts file access to within `cwd` boundaries, preventing path traversal attacks. Default is `true`
- `logger?` - A logger instance compatible with Pino logger interface for debugging and monitoring. Default is `undefined`
- `cache.ttl?` - The time to live for the cache in milliseconds. Default is `0` which means no expiration

@@ -140,2 +147,3 @@ - `cache.lruSize?` - The number of items to keep in the cache. Default is `0` which means no limit

- `strictPaths: boolean` - If `true` restricts file access to within `cwd` boundaries. Default is `true`
- `logger: ILogger | undefined` - A logger instance for debugging and monitoring cache operations
- `createFileKey(filePath: string): string` - Returns the cache key for the file path (returns the path exactly as provided).

@@ -402,5 +410,5 @@ - `deleteCacheFile(): boolean` - Deletes the cache file from disk

In the past we have seen people do random values on the `meta` object. This can cause issues with the `meta` object. To avoid this we have `data` which can be anything.
In the past we have seen people do random values on the `meta` object. This can cause issues with the `meta` object. To avoid this we have `data` which can be anything.
```javascript
```javascript
const fileEntryCache = new FileEntryCache();

@@ -410,2 +418,121 @@ const fileDescriptor = fileEntryCache.getFileDescriptor('file.txt');

```
# Logger Support
The `FileEntryCache` supports logging through a Pino-compatible logger interface. This is useful for debugging and monitoring cache operations in production environments.
## Logger Interface
The logger must implement the following interface:
```typescript
interface ILogger {
level?: string;
trace: (message: string | object, ...args: unknown[]) => void;
debug: (message: string | object, ...args: unknown[]) => void;
info: (message: string | object, ...args: unknown[]) => void;
warn: (message: string | object, ...args: unknown[]) => void;
error: (message: string | object, ...args: unknown[]) => void;
fatal: (message: string | object, ...args: unknown[]) => void;
}
```
## Using Pino Logger
You can pass a Pino logger instance to the `FileEntryCache` constructor or set it via the `logger` property:
```javascript
import pino from 'pino';
import fileEntryCache from 'file-entry-cache';
// Create a Pino logger
const logger = pino({
level: 'debug',
transport: {
target: 'pino-pretty',
options: {
colorize: true
}
}
});
// Pass logger in constructor
const cache = new fileEntryCache.FileEntryCache({
logger,
cacheId: 'my-cache'
});
// Or set it after creation
cache.logger = logger;
// Now all cache operations will be logged
const descriptor = cache.getFileDescriptor('./src/file.txt');
```
## Log Levels
The logger will output different levels of information:
- **trace**: Detailed internal operations (key creation, cached meta lookup, file stats)
- **debug**: Method entry, checksum settings, change detection, file status
- **info**: Important state changes (file has changed)
- **error**: File read errors and exceptions
## Example with Custom Log Levels
```javascript
import pino from 'pino';
import { FileEntryCache } from 'file-entry-cache';
// Create logger with specific level
const logger = pino({ level: 'info' });
const cache = new FileEntryCache({
logger,
useCheckSum: true
});
// This will log at info level when files change
const files = ['./src/index.js', './src/utils.js'];
files.forEach(file => {
const descriptor = cache.getFileDescriptor(file);
if (descriptor.changed) {
console.log(`Processing changed file: ${file}`);
}
});
cache.reconcile();
```
## Debugging Cache Operations
For detailed debugging, set the logger level to `debug` or `trace`:
```javascript
import pino from 'pino';
import { FileEntryCache } from 'file-entry-cache';
const logger = pino({
level: 'trace',
transport: {
target: 'pino-pretty'
}
});
const cache = new FileEntryCache({
logger,
useCheckSum: true,
cwd: '/project/root'
});
// Will log detailed information about:
// - File path resolution
// - Cache key creation
// - Cached metadata lookup
// - File stats reading
// - Hash calculation (if using checksums)
// - Change detection logic
const descriptor = cache.getFileDescriptor('./src/app.js');
```
# How to Contribute

@@ -412,0 +539,0 @@