file-entry-cache 11.1.2 → 11.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,519 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { FlatCache, createFromFile as createFromFile$1 } from "flat-cache";
5
+ //#region src/index.ts
6
+ /**
7
+ * Create a new FileEntryCache instance from a file path
8
+ * @param filePath - The path to the cache file
9
+ * @param options - create options such as useChecksum, cwd, and more
10
+ * @returns A new FileEntryCache instance
11
+ */
12
+ function createFromFile(filePath, options) {
13
+ return create(path.basename(filePath), path.dirname(filePath), options);
14
+ }
15
+ /**
16
+ * Create a new FileEntryCache instance
17
+ * @param cacheId - The cache file name
18
+ * @param cacheDirectory - The directory to store the cache file (default: undefined, cache won't be persisted)
19
+ * @param options - Whether to use checksum to detect file changes (default: false)
20
+ * @returns A new FileEntryCache instance
21
+ */
22
+ function create(cacheId, cacheDirectory, options) {
23
+ const opts = {
24
+ ...options,
25
+ cache: {
26
+ cacheId,
27
+ cacheDir: cacheDirectory
28
+ }
29
+ };
30
+ const fileEntryCache = new FileEntryCache(opts);
31
+ if (cacheDirectory) {
32
+ const cachePath = `${cacheDirectory}/${cacheId}`;
33
+ if (fs.existsSync(cachePath)) try {
34
+ fileEntryCache.cache = createFromFile$1(cachePath, opts.cache);
35
+ } catch (error) {
36
+ if (error instanceof SyntaxError || error instanceof TypeError) fileEntryCache.cache = new FlatCache(opts.cache);
37
+ else throw error;
38
+ }
39
+ }
40
+ return fileEntryCache;
41
+ }
42
+ var FileEntryDefault = class {
43
+ static create = create;
44
+ static createFromFile = createFromFile;
45
+ };
46
+ var FileEntryCache = class {
47
+ _cache = new FlatCache({ useClone: false });
48
+ _useCheckSum = false;
49
+ _hashAlgorithm = "md5";
50
+ _cwd = process.cwd();
51
+ _restrictAccessToCwd = false;
52
+ _logger;
53
+ _useAbsolutePathAsKey = false;
54
+ _useModifiedTime = true;
55
+ /**
56
+ * Snapshot of the persisted meta for each key as of the last load/reconcile.
57
+ * Change detection compares against this baseline (not the working cache) so
58
+ * that repeated `getFileDescriptor()` calls keep reporting a file as changed
59
+ * until the cache is reconciled. The set of keys also tracks which files were
60
+ * visited during the current session so that `reconcile()` only updates those.
61
+ */
62
+ _originalMeta = /* @__PURE__ */ new Map();
63
+ /**
64
+ * Create a new FileEntryCache instance
65
+ * @param options - The options for the FileEntryCache (all properties are optional with defaults)
66
+ */
67
+ constructor(options) {
68
+ if (options?.cache) this._cache = new FlatCache(options.cache);
69
+ if (options?.useCheckSum) this._useCheckSum = options.useCheckSum;
70
+ if (options?.hashAlgorithm) this._hashAlgorithm = options.hashAlgorithm;
71
+ if (options?.cwd) this._cwd = options.cwd;
72
+ if (options?.useModifiedTime !== void 0) this._useModifiedTime = options.useModifiedTime;
73
+ if (options?.restrictAccessToCwd !== void 0) this._restrictAccessToCwd = options.restrictAccessToCwd;
74
+ if (options?.useAbsolutePathAsKey !== void 0) this._useAbsolutePathAsKey = options.useAbsolutePathAsKey;
75
+ if (options?.logger) this._logger = options.logger;
76
+ }
77
+ /**
78
+ * Get the cache
79
+ * @returns {FlatCache} The cache
80
+ */
81
+ get cache() {
82
+ return this._cache;
83
+ }
84
+ /**
85
+ * Set the cache
86
+ * @param {FlatCache} cache - The cache to set
87
+ */
88
+ set cache(cache) {
89
+ this._cache = cache;
90
+ this._originalMeta = /* @__PURE__ */ new Map();
91
+ }
92
+ /**
93
+ * Get the logger
94
+ * @returns {ILogger | undefined} The logger instance
95
+ */
96
+ get logger() {
97
+ return this._logger;
98
+ }
99
+ /**
100
+ * Set the logger
101
+ * @param {ILogger | undefined} logger - The logger to set
102
+ */
103
+ set logger(logger) {
104
+ this._logger = logger;
105
+ }
106
+ /**
107
+ * Use the hash to check if the file has changed
108
+ * @returns {boolean} if the hash is used to check if the file has changed (default: false)
109
+ */
110
+ get useCheckSum() {
111
+ return this._useCheckSum;
112
+ }
113
+ /**
114
+ * Set the useCheckSum value
115
+ * @param {boolean} value - The value to set
116
+ */
117
+ set useCheckSum(value) {
118
+ this._useCheckSum = value;
119
+ }
120
+ /**
121
+ * Get the hash algorithm
122
+ * @returns {string} The hash algorithm (default: 'md5')
123
+ */
124
+ get hashAlgorithm() {
125
+ return this._hashAlgorithm;
126
+ }
127
+ /**
128
+ * Set the hash algorithm
129
+ * @param {string} value - The value to set
130
+ */
131
+ set hashAlgorithm(value) {
132
+ this._hashAlgorithm = value;
133
+ }
134
+ /**
135
+ * Get the current working directory
136
+ * @returns {string} The current working directory (default: process.cwd())
137
+ */
138
+ get cwd() {
139
+ return this._cwd;
140
+ }
141
+ /**
142
+ * Set the current working directory
143
+ *
144
+ * Note: when relative paths are used as cache keys (the default), `cwd` must
145
+ * stay stable across a `getFileDescriptor()` / `reconcile()` cycle. Relative
146
+ * keys are resolved against the *current* `cwd` each time, so changing it
147
+ * mid-run can cause `reconcile()` to resolve a key to a different (missing)
148
+ * path and drop the entry. Use absolute keys (`useAbsolutePathAsKey: true`)
149
+ * if `cwd` must change during a run.
150
+ * @param {string} value - The value to set
151
+ */
152
+ set cwd(value) {
153
+ this._cwd = value;
154
+ }
155
+ /**
156
+ * Get whether to use modified time for change detection
157
+ * @returns {boolean} Whether modified time (mtime) is used for change detection (default: true)
158
+ */
159
+ get useModifiedTime() {
160
+ return this._useModifiedTime;
161
+ }
162
+ /**
163
+ * Set whether to use modified time for change detection
164
+ * @param {boolean} value - The value to set
165
+ */
166
+ set useModifiedTime(value) {
167
+ this._useModifiedTime = value;
168
+ }
169
+ /**
170
+ * Get whether to restrict paths to cwd boundaries
171
+ * @returns {boolean} Whether strict path checking is enabled (default: true)
172
+ */
173
+ get restrictAccessToCwd() {
174
+ return this._restrictAccessToCwd;
175
+ }
176
+ /**
177
+ * Set whether to restrict paths to cwd boundaries
178
+ * @param {boolean} value - The value to set
179
+ */
180
+ set restrictAccessToCwd(value) {
181
+ this._restrictAccessToCwd = value;
182
+ }
183
+ /**
184
+ * Get whether to use absolute path as cache key
185
+ * @returns {boolean} Whether cache keys use absolute paths (default: false)
186
+ */
187
+ get useAbsolutePathAsKey() {
188
+ return this._useAbsolutePathAsKey;
189
+ }
190
+ /**
191
+ * Set whether to use absolute path as cache key
192
+ * @param {boolean} value - The value to set
193
+ */
194
+ set useAbsolutePathAsKey(value) {
195
+ this._useAbsolutePathAsKey = value;
196
+ }
197
+ /**
198
+ * Given a buffer, calculate md5 hash of its content.
199
+ * @method getHash
200
+ * @param {Buffer} buffer buffer to calculate hash on
201
+ * @return {String} content hash digest
202
+ */
203
+ getHash(buffer) {
204
+ return crypto.createHash(this._hashAlgorithm).update(buffer).digest("hex");
205
+ }
206
+ /**
207
+ * Create the key for the file path used for caching.
208
+ * @method createFileKey
209
+ * @param {String} filePath
210
+ * @return {String}
211
+ */
212
+ createFileKey(filePath) {
213
+ let result = filePath;
214
+ if (this._useAbsolutePathAsKey && this.isRelativePath(filePath)) result = this.getAbsolutePathWithCwd(filePath, this._cwd);
215
+ return result;
216
+ }
217
+ /**
218
+ * Check if the file path is a relative path
219
+ * @method isRelativePath
220
+ * @param filePath - The file path to check
221
+ * @returns {boolean} if the file path is a relative path, false otherwise
222
+ */
223
+ isRelativePath(filePath) {
224
+ return !path.isAbsolute(filePath);
225
+ }
226
+ /**
227
+ * Delete the cache file from the disk
228
+ * @method deleteCacheFile
229
+ * @return {boolean} true if the file was deleted, false otherwise
230
+ */
231
+ deleteCacheFile() {
232
+ return this._cache.removeCacheFile();
233
+ }
234
+ /**
235
+ * Remove the cache from the file and clear the memory cache
236
+ * @method destroy
237
+ */
238
+ destroy() {
239
+ this._cache.destroy();
240
+ this._originalMeta = /* @__PURE__ */ new Map();
241
+ }
242
+ /**
243
+ * Remove and Entry From the Cache
244
+ * @method removeEntry
245
+ * @param filePath - The file path to remove from the cache
246
+ */
247
+ removeEntry(filePath) {
248
+ const key = this.createFileKey(filePath);
249
+ this._cache.removeKey(key);
250
+ this._originalMeta.delete(key);
251
+ }
252
+ /**
253
+ * Reconcile the cache
254
+ * @method reconcile
255
+ */
256
+ reconcile() {
257
+ for (const key of [...this._cache.keys()]) try {
258
+ fs.statSync(this.getAbsolutePath(key));
259
+ } catch (error) {
260
+ if (error.code === "ENOENT") {
261
+ this._cache.removeKey(key);
262
+ this._originalMeta.delete(key);
263
+ } else this._logger?.error({
264
+ key,
265
+ error
266
+ }, "reconcile: unable to stat file; keeping cached entry");
267
+ }
268
+ for (const key of [...this._originalMeta.keys()]) {
269
+ const meta = this._cache.getKey(key);
270
+ if (meta) this._originalMeta.set(key, { ...meta });
271
+ else this._originalMeta.delete(key);
272
+ }
273
+ this._cache.save();
274
+ }
275
+ /**
276
+ * Check if the file has changed
277
+ * @method hasFileChanged
278
+ * @param filePath - The file path to check
279
+ * @returns {boolean} if the file has changed, false otherwise
280
+ */
281
+ hasFileChanged(filePath) {
282
+ let result = false;
283
+ const fileDescriptor = this.getFileDescriptor(filePath);
284
+ if ((!fileDescriptor.err || !fileDescriptor.notFound) && fileDescriptor.changed) result = true;
285
+ return result;
286
+ }
287
+ /**
288
+ * Get the file descriptor for the file path
289
+ * @method getFileDescriptor
290
+ * @param filePath - The file path to get the file descriptor for
291
+ * @param options - The options for getting the file descriptor
292
+ * @returns The file descriptor
293
+ */
294
+ getFileDescriptor(filePath, options) {
295
+ this._logger?.debug({
296
+ filePath,
297
+ options
298
+ }, "Getting file descriptor");
299
+ let fstat;
300
+ const result = {
301
+ key: this.createFileKey(filePath),
302
+ changed: false,
303
+ meta: {}
304
+ };
305
+ this._logger?.trace({ key: result.key }, "Created file key");
306
+ const metaCache = this._cache.getKey(result.key);
307
+ if (metaCache) this._logger?.trace({ metaCache }, "Found cached meta");
308
+ else this._logger?.trace("No cached meta found");
309
+ result.meta = metaCache ? { ...metaCache } : {};
310
+ const absolutePath = this.getAbsolutePath(filePath);
311
+ this._logger?.trace({ absolutePath }, "Resolved absolute path");
312
+ const useCheckSumValue = options?.useCheckSum ?? this._useCheckSum;
313
+ this._logger?.debug({ useCheckSum: useCheckSumValue }, "Using checksum setting");
314
+ const useModifiedTimeValue = options?.useModifiedTime ?? this.useModifiedTime;
315
+ this._logger?.debug({ useModifiedTime: useModifiedTimeValue }, "Using modified time (mtime) setting");
316
+ try {
317
+ fstat = fs.statSync(absolutePath);
318
+ result.meta.size = fstat.size;
319
+ result.meta.mtime = fstat.mtime.getTime();
320
+ this._logger?.trace({
321
+ size: result.meta.size,
322
+ mtime: result.meta.mtime
323
+ }, "Read file stats");
324
+ if (useCheckSumValue) {
325
+ const buffer = fs.readFileSync(absolutePath);
326
+ result.meta.hash = this.getHash(buffer);
327
+ this._logger?.trace({ hash: result.meta.hash }, "Calculated file hash");
328
+ }
329
+ } catch (error) {
330
+ this._logger?.error({
331
+ filePath,
332
+ error
333
+ }, "Error reading file");
334
+ this.removeEntry(filePath);
335
+ let notFound = false;
336
+ if (error.message.includes("ENOENT")) {
337
+ notFound = true;
338
+ this._logger?.debug({ filePath }, "File not found");
339
+ }
340
+ return {
341
+ key: result.key,
342
+ err: error,
343
+ notFound,
344
+ meta: {}
345
+ };
346
+ }
347
+ if (!this._originalMeta.has(result.key)) this._originalMeta.set(result.key, metaCache ? { ...metaCache } : void 0);
348
+ const baseline = this._originalMeta.get(result.key);
349
+ if (baseline === void 0) {
350
+ result.changed = true;
351
+ this._cache.setKey(result.key, result.meta);
352
+ this._logger?.debug({ filePath }, "File not in cache, marked as changed");
353
+ return result;
354
+ }
355
+ if (useModifiedTimeValue && baseline.mtime !== result.meta?.mtime) {
356
+ result.changed = true;
357
+ this._logger?.debug({
358
+ filePath,
359
+ oldMtime: baseline.mtime,
360
+ newMtime: result.meta.mtime
361
+ }, "File changed: mtime differs");
362
+ }
363
+ if (baseline.size !== result.meta?.size) {
364
+ result.changed = true;
365
+ this._logger?.debug({
366
+ filePath,
367
+ oldSize: baseline.size,
368
+ newSize: result.meta.size
369
+ }, "File changed: size differs");
370
+ }
371
+ if (useCheckSumValue && baseline.hash !== result.meta?.hash) {
372
+ result.changed = true;
373
+ this._logger?.debug({
374
+ filePath,
375
+ oldHash: baseline.hash,
376
+ newHash: result.meta.hash
377
+ }, "File changed: hash differs");
378
+ }
379
+ this._cache.setKey(result.key, result.meta);
380
+ if (result.changed) this._logger?.info({ filePath }, "File has changed");
381
+ else this._logger?.debug({ filePath }, "File unchanged");
382
+ return result;
383
+ }
384
+ /**
385
+ * Get the file descriptors for the files
386
+ * @method normalizeEntries
387
+ * @param files?: string[] - The files to get the file descriptors for
388
+ * @returns The file descriptors
389
+ */
390
+ normalizeEntries(files) {
391
+ const result = [];
392
+ if (files) {
393
+ for (const file of files) {
394
+ const fileDescriptor = this.getFileDescriptor(file);
395
+ result.push(fileDescriptor);
396
+ }
397
+ return result;
398
+ }
399
+ const keys = this.cache.keys();
400
+ for (const key of keys) {
401
+ const fileDescriptor = this.getFileDescriptor(key);
402
+ if (!fileDescriptor.notFound && !fileDescriptor.err) result.push(fileDescriptor);
403
+ }
404
+ return result;
405
+ }
406
+ /**
407
+ * Analyze the files
408
+ * @method analyzeFiles
409
+ * @param files - The files to analyze
410
+ * @returns {AnalyzedFiles} The analysis of the files
411
+ */
412
+ analyzeFiles(files) {
413
+ const result = {
414
+ changedFiles: [],
415
+ notFoundFiles: [],
416
+ notChangedFiles: []
417
+ };
418
+ const fileDescriptors = this.normalizeEntries(files);
419
+ for (const fileDescriptor of fileDescriptors) if (fileDescriptor.notFound) result.notFoundFiles.push(fileDescriptor.key);
420
+ else if (fileDescriptor.changed) result.changedFiles.push(fileDescriptor.key);
421
+ else result.notChangedFiles.push(fileDescriptor.key);
422
+ return result;
423
+ }
424
+ /**
425
+ * Get the updated files
426
+ * @method getUpdatedFiles
427
+ * @param files - The files to get the updated files for
428
+ * @returns {string[]} The updated files
429
+ */
430
+ getUpdatedFiles(files) {
431
+ const result = [];
432
+ const fileDescriptors = this.normalizeEntries(files);
433
+ for (const fileDescriptor of fileDescriptors) if (fileDescriptor.changed) result.push(fileDescriptor.key);
434
+ return result;
435
+ }
436
+ /**
437
+ * Get the file descriptors by path prefix
438
+ * @method getFileDescriptorsByPath
439
+ * @param filePath - the path prefix to match
440
+ * @returns {FileDescriptor[]} The file descriptors
441
+ */
442
+ getFileDescriptorsByPath(filePath) {
443
+ const result = [];
444
+ const keys = this._cache.keys();
445
+ for (const key of keys)
446
+ /* v8 ignore next -- @preserve */
447
+ if (key.startsWith(filePath)) {
448
+ const fileDescriptor = this.getFileDescriptor(key);
449
+ result.push(fileDescriptor);
450
+ }
451
+ return result;
452
+ }
453
+ /**
454
+ * Get the Absolute Path. If it is already absolute it will return the path as is.
455
+ * When restrictAccessToCwd is enabled, ensures the resolved path stays within cwd boundaries.
456
+ * @method getAbsolutePath
457
+ * @param filePath - The file path to get the absolute path for
458
+ * @returns {string}
459
+ * @throws {Error} When restrictAccessToCwd is true and path would resolve outside cwd
460
+ */
461
+ getAbsolutePath(filePath) {
462
+ if (this.isRelativePath(filePath)) {
463
+ const sanitizedPath = filePath.replace(/\0/g, "");
464
+ const resolved = path.resolve(this._cwd, sanitizedPath);
465
+ if (this._restrictAccessToCwd) {
466
+ const normalizedResolved = path.normalize(resolved);
467
+ const normalizedCwd = path.normalize(this._cwd);
468
+ if (!(normalizedResolved === normalizedCwd || normalizedResolved.startsWith(normalizedCwd + path.sep))) throw new Error(`Path traversal attempt blocked: "${filePath}" resolves outside of working directory "${this._cwd}"`);
469
+ }
470
+ return resolved;
471
+ }
472
+ return filePath;
473
+ }
474
+ /**
475
+ * Get the Absolute Path with a custom working directory. If it is already absolute it will return the path as is.
476
+ * When restrictAccessToCwd is enabled, ensures the resolved path stays within the provided cwd boundaries.
477
+ * @method getAbsolutePathWithCwd
478
+ * @param filePath - The file path to get the absolute path for
479
+ * @param cwd - The custom working directory to resolve relative paths from
480
+ * @returns {string}
481
+ * @throws {Error} When restrictAccessToCwd is true and path would resolve outside the provided cwd
482
+ */
483
+ getAbsolutePathWithCwd(filePath, cwd) {
484
+ if (this.isRelativePath(filePath)) {
485
+ const sanitizedPath = filePath.replace(/\0/g, "");
486
+ const resolved = path.resolve(cwd, sanitizedPath);
487
+ if (this._restrictAccessToCwd) {
488
+ const normalizedResolved = path.normalize(resolved);
489
+ const normalizedCwd = path.normalize(cwd);
490
+ if (!(normalizedResolved === normalizedCwd || normalizedResolved.startsWith(normalizedCwd + path.sep))) throw new Error(`Path traversal attempt blocked: "${filePath}" resolves outside of working directory "${cwd}"`);
491
+ }
492
+ return resolved;
493
+ }
494
+ return filePath;
495
+ }
496
+ /**
497
+ * Rename cache keys that start with a given path prefix.
498
+ * @method renameCacheKeys
499
+ * @param oldPath - The old path prefix to rename
500
+ * @param newPath - The new path prefix to rename to
501
+ */
502
+ renameCacheKeys(oldPath, newPath) {
503
+ const keys = this._cache.keys();
504
+ for (const key of keys)
505
+ /* v8 ignore next -- @preserve */
506
+ if (key.startsWith(oldPath)) {
507
+ const newKey = key.replace(oldPath, newPath);
508
+ const meta = this._cache.getKey(key);
509
+ this._cache.removeKey(key);
510
+ this._cache.setKey(newKey, meta);
511
+ if (this._originalMeta.has(key)) {
512
+ this._originalMeta.set(newKey, this._originalMeta.get(key));
513
+ this._originalMeta.delete(key);
514
+ }
515
+ }
516
+ }
517
+ };
518
+ //#endregion
519
+ export { FileEntryCache, create, createFromFile, FileEntryDefault as default };
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "file-entry-cache",
3
- "version": "11.1.2",
3
+ "version": "11.1.5",
4
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",
5
5
  "type": "module",
6
- "main": "./dist/index.js",
7
- "module": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.mts",
9
9
  "exports": {
10
10
  ".": {
11
11
  "import": {
12
- "types": "./dist/index.d.ts",
13
- "default": "./dist/index.js"
12
+ "types": "./dist/index.d.mts",
13
+ "default": "./dist/index.mjs"
14
14
  },
15
15
  "require": {
16
16
  "types": "./dist/index.d.cts",
@@ -35,25 +35,19 @@
35
35
  "cache"
36
36
  ],
37
37
  "devDependencies": {
38
- "@biomejs/biome": "^2.3.11",
39
- "@types/node": "^25.0.8",
40
- "@vitest/coverage-v8": "^4.0.17",
41
- "pino": "^10.1.1",
42
- "rimraf": "^6.1.2",
43
- "tsup": "^8.5.1",
44
- "typescript": "^5.9.3",
45
- "vitest": "^4.0.17"
38
+ "pino": "^10.3.1",
39
+ "tsdown": "^0.22.0",
40
+ "typescript": "^5.9.3"
46
41
  },
47
42
  "dependencies": {
48
- "flat-cache": "^6.1.20"
43
+ "flat-cache": "^6.1.23"
49
44
  },
50
45
  "files": [
51
46
  "dist",
52
47
  "license"
53
48
  ],
54
49
  "scripts": {
55
- "build": "rimraf ./dist && tsup src/index.ts --format cjs,esm --dts --clean",
56
- "prepublish": "pnpm build",
50
+ "build": "rimraf ./dist && tsdown src/index.ts --format cjs,esm --dts --clean",
57
51
  "lint": "biome check --write --error-on-warnings",
58
52
  "test": "pnpm lint && vitest run --coverage",
59
53
  "test:ci": "biome check --error-on-warnings && vitest run --coverage",