tailwindcss-patch 9.0.0-alpha.1 → 9.0.0-alpha.2

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.
Files changed (70) hide show
  1. package/README.md +20 -0
  2. package/dist/{chunk-Z6OMJZTU.js → chunk-TOAZIPHJ.js} +29 -11
  3. package/dist/{chunk-SWLOK2S6.mjs → chunk-VDWTCQ74.mjs} +29 -11
  4. package/dist/cli.js +4 -4
  5. package/dist/cli.mjs +1 -1
  6. package/dist/index.d.mts +43 -35
  7. package/dist/index.d.ts +43 -35
  8. package/dist/index.js +2 -2
  9. package/dist/index.mjs +1 -1
  10. package/package.json +8 -3
  11. package/src/api/tailwindcss-patcher.ts +424 -0
  12. package/src/babel/index.ts +12 -0
  13. package/src/cache/context.ts +212 -0
  14. package/src/cache/store.ts +1440 -0
  15. package/src/cache/types.ts +71 -0
  16. package/src/cli.ts +20 -0
  17. package/src/commands/basic-handlers.ts +145 -0
  18. package/src/commands/cli.ts +56 -0
  19. package/src/commands/command-context.ts +77 -0
  20. package/src/commands/command-definitions.ts +102 -0
  21. package/src/commands/command-metadata.ts +68 -0
  22. package/src/commands/command-registrar.ts +39 -0
  23. package/src/commands/command-runtime.ts +33 -0
  24. package/src/commands/default-handler-map.ts +25 -0
  25. package/src/commands/migrate-config.ts +104 -0
  26. package/src/commands/migrate-handler.ts +67 -0
  27. package/src/commands/migration-aggregation.ts +100 -0
  28. package/src/commands/migration-args.ts +85 -0
  29. package/src/commands/migration-file-executor.ts +189 -0
  30. package/src/commands/migration-output.ts +115 -0
  31. package/src/commands/migration-report-loader.ts +26 -0
  32. package/src/commands/migration-report.ts +21 -0
  33. package/src/commands/migration-source.ts +318 -0
  34. package/src/commands/migration-target-files.ts +161 -0
  35. package/src/commands/migration-target-resolver.ts +34 -0
  36. package/src/commands/migration-types.ts +65 -0
  37. package/src/commands/restore-handler.ts +24 -0
  38. package/src/commands/status-handler.ts +17 -0
  39. package/src/commands/status-output.ts +60 -0
  40. package/src/commands/token-output.ts +30 -0
  41. package/src/commands/types.ts +137 -0
  42. package/src/commands/validate-handler.ts +42 -0
  43. package/src/commands/validate.ts +83 -0
  44. package/src/config/index.ts +25 -0
  45. package/src/config/workspace.ts +87 -0
  46. package/src/constants.ts +4 -0
  47. package/src/extraction/candidate-extractor.ts +354 -0
  48. package/src/index.ts +57 -0
  49. package/src/install/class-collector.ts +1 -0
  50. package/src/install/context-registry.ts +1 -0
  51. package/src/install/index.ts +5 -0
  52. package/src/install/patch-runner.ts +1 -0
  53. package/src/install/process-tailwindcss.ts +1 -0
  54. package/src/install/status.ts +1 -0
  55. package/src/logger.ts +5 -0
  56. package/src/options/legacy.ts +93 -0
  57. package/src/options/normalize.ts +262 -0
  58. package/src/options/types.ts +217 -0
  59. package/src/patching/operations/export-context/index.ts +110 -0
  60. package/src/patching/operations/export-context/postcss-v2.ts +235 -0
  61. package/src/patching/operations/export-context/postcss-v3.ts +249 -0
  62. package/src/patching/operations/extend-length-units.ts +197 -0
  63. package/src/patching/patch-runner.ts +46 -0
  64. package/src/patching/status.ts +262 -0
  65. package/src/runtime/class-collector.ts +105 -0
  66. package/src/runtime/collector.ts +148 -0
  67. package/src/runtime/context-registry.ts +65 -0
  68. package/src/runtime/process-tailwindcss.ts +115 -0
  69. package/src/types.ts +159 -0
  70. package/src/utils.ts +52 -0
@@ -0,0 +1,1440 @@
1
+ import type { NormalizedCacheOptions } from '../options/types'
2
+ import type {
3
+ CacheClearOptions,
4
+ CacheClearResult,
5
+ CacheContextDescriptor,
6
+ CacheIndexEntry,
7
+ CacheIndexFileV2,
8
+ CacheReadMeta,
9
+ CacheReadResult,
10
+ } from './types'
11
+ import process from 'node:process'
12
+ import fs from 'fs-extra'
13
+ import logger from '../logger'
14
+ import { explainContextMismatch } from './context'
15
+ import { CACHE_SCHEMA_VERSION } from './types'
16
+
17
+ interface ParsedCacheFileV2 {
18
+ kind: 'v2'
19
+ data: CacheIndexFileV2
20
+ }
21
+
22
+ interface ParsedCacheFileLegacy {
23
+ kind: 'legacy'
24
+ data: string[]
25
+ }
26
+
27
+ interface ParsedCacheFileEmpty {
28
+ kind: 'empty'
29
+ }
30
+
31
+ interface ParsedCacheFileInvalid {
32
+ kind: 'invalid'
33
+ }
34
+
35
+ type ParsedCacheFile = ParsedCacheFileV2 | ParsedCacheFileLegacy | ParsedCacheFileEmpty | ParsedCacheFileInvalid
36
+
37
+ function isErrnoException(error: unknown): error is NodeJS.ErrnoException {
38
+ return error instanceof Error && typeof (error as NodeJS.ErrnoException).code === 'string'
39
+ }
40
+
41
+ function isAccessDenied(error: unknown): error is NodeJS.ErrnoException {
42
+ return isErrnoException(error)
43
+ && Boolean(error.code && ['EPERM', 'EBUSY', 'EACCES'].includes(error.code))
44
+ }
45
+
46
+ function toStringArray(value: unknown): string[] {
47
+ if (!Array.isArray(value)) {
48
+ return []
49
+ }
50
+
51
+ return value.filter((item): item is string => typeof item === 'string')
52
+ }
53
+
54
+ function asObject(value: unknown): Record<string, unknown> | undefined {
55
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
56
+ return undefined
57
+ }
58
+ return value as Record<string, unknown>
59
+ }
60
+
61
+ function toReadMeta(meta: CacheReadMeta): CacheReadMeta {
62
+ return {
63
+ ...meta,
64
+ details: [...meta.details],
65
+ }
66
+ }
67
+
68
+ function cloneEntry(entry: CacheIndexEntry): CacheIndexEntry {
69
+ return {
70
+ context: {
71
+ ...entry.context,
72
+ },
73
+ values: [...entry.values],
74
+ updatedAt: entry.updatedAt,
75
+ }
76
+ }
77
+
78
+ export class CacheStore {
79
+ private readonly driver: NormalizedCacheOptions['driver']
80
+ private readonly lockPath: string
81
+ private memoryCache: Set<string> | null = null
82
+ private memoryIndex: CacheIndexFileV2 | null = null
83
+ private lastReadMeta: CacheReadMeta = {
84
+ hit: false,
85
+ reason: 'context-not-found',
86
+ details: [],
87
+ }
88
+
89
+ constructor(
90
+ private readonly options: NormalizedCacheOptions,
91
+ private readonly context?: CacheContextDescriptor,
92
+ ) {
93
+ this.driver = options.driver ?? 'file'
94
+ this.lockPath = `${this.options.path}.lock`
95
+ }
96
+
97
+ private isContextAware() {
98
+ return this.context !== undefined
99
+ }
100
+
101
+ private createEmptyIndex(): CacheIndexFileV2 {
102
+ return {
103
+ schemaVersion: CACHE_SCHEMA_VERSION,
104
+ updatedAt: new Date().toISOString(),
105
+ contexts: {},
106
+ }
107
+ }
108
+
109
+ private async ensureDir() {
110
+ await fs.ensureDir(this.options.dir)
111
+ }
112
+
113
+ private ensureDirSync() {
114
+ fs.ensureDirSync(this.options.dir)
115
+ }
116
+
117
+ private createTempPath() {
118
+ const uniqueSuffix = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`
119
+ return `${this.options.path}.${uniqueSuffix}.tmp`
120
+ }
121
+
122
+ private async replaceCacheFile(tempPath: string): Promise<boolean> {
123
+ try {
124
+ await fs.rename(tempPath, this.options.path)
125
+ return true
126
+ }
127
+ catch (error) {
128
+ if (isErrnoException(error) && (error.code === 'EEXIST' || error.code === 'EPERM')) {
129
+ try {
130
+ await fs.remove(this.options.path)
131
+ }
132
+ catch (removeError) {
133
+ if (isAccessDenied(removeError)) {
134
+ logger.debug('Tailwind class cache locked or read-only, skipping update.', removeError)
135
+ return false
136
+ }
137
+
138
+ if (!isErrnoException(removeError) || removeError.code !== 'ENOENT') {
139
+ throw removeError
140
+ }
141
+ }
142
+
143
+ await fs.rename(tempPath, this.options.path)
144
+ return true
145
+ }
146
+
147
+ throw error
148
+ }
149
+ }
150
+
151
+ private replaceCacheFileSync(tempPath: string): boolean {
152
+ try {
153
+ fs.renameSync(tempPath, this.options.path)
154
+ return true
155
+ }
156
+ catch (error) {
157
+ if (isErrnoException(error) && (error.code === 'EEXIST' || error.code === 'EPERM')) {
158
+ try {
159
+ fs.removeSync(this.options.path)
160
+ }
161
+ catch (removeError) {
162
+ if (isAccessDenied(removeError)) {
163
+ logger.debug('Tailwind class cache locked or read-only, skipping update.', removeError)
164
+ return false
165
+ }
166
+
167
+ if (!isErrnoException(removeError) || removeError.code !== 'ENOENT') {
168
+ throw removeError
169
+ }
170
+ }
171
+
172
+ fs.renameSync(tempPath, this.options.path)
173
+ return true
174
+ }
175
+
176
+ throw error
177
+ }
178
+ }
179
+
180
+ private async cleanupTempFile(tempPath: string) {
181
+ try {
182
+ await fs.remove(tempPath)
183
+ }
184
+ catch {}
185
+ }
186
+
187
+ private cleanupTempFileSync(tempPath: string) {
188
+ try {
189
+ fs.removeSync(tempPath)
190
+ }
191
+ catch {}
192
+ }
193
+
194
+ private async delay(ms: number) {
195
+ await new Promise(resolve => setTimeout(resolve, ms))
196
+ }
197
+
198
+ private async acquireLock(): Promise<boolean> {
199
+ await fs.ensureDir(this.options.dir)
200
+ const maxAttempts = 40
201
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
202
+ try {
203
+ await fs.writeFile(this.lockPath, `${process.pid}\n${Date.now()}`, { flag: 'wx' })
204
+ return true
205
+ }
206
+ catch (error) {
207
+ if (!isErrnoException(error) || error.code !== 'EEXIST') {
208
+ logger.debug('Unable to acquire cache lock.', error)
209
+ return false
210
+ }
211
+
212
+ try {
213
+ const stat = await fs.stat(this.lockPath)
214
+ if (Date.now() - stat.mtimeMs > 30_000) {
215
+ await fs.remove(this.lockPath)
216
+ continue
217
+ }
218
+ }
219
+ catch {}
220
+
221
+ await this.delay(25)
222
+ }
223
+ }
224
+
225
+ logger.debug('Timed out while waiting for cache lock; skipping cache mutation.')
226
+ return false
227
+ }
228
+
229
+ private releaseLockSyncOrAsync(sync: true): void
230
+ private releaseLockSyncOrAsync(sync: false): Promise<void>
231
+ private releaseLockSyncOrAsync(sync: boolean): void | Promise<void> {
232
+ if (sync) {
233
+ try {
234
+ fs.removeSync(this.lockPath)
235
+ }
236
+ catch {}
237
+ return
238
+ }
239
+
240
+ return fs.remove(this.lockPath).catch(() => undefined)
241
+ }
242
+
243
+ private acquireLockSync(): boolean {
244
+ fs.ensureDirSync(this.options.dir)
245
+ const maxAttempts = 40
246
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
247
+ try {
248
+ fs.writeFileSync(this.lockPath, `${process.pid}\n${Date.now()}`, { flag: 'wx' })
249
+ return true
250
+ }
251
+ catch (error) {
252
+ if (!isErrnoException(error) || error.code !== 'EEXIST') {
253
+ logger.debug('Unable to acquire cache lock.', error)
254
+ return false
255
+ }
256
+
257
+ try {
258
+ const stat = fs.statSync(this.lockPath)
259
+ if (Date.now() - stat.mtimeMs > 30_000) {
260
+ fs.removeSync(this.lockPath)
261
+ continue
262
+ }
263
+ }
264
+ catch {}
265
+
266
+ const start = Date.now()
267
+ while (Date.now() - start < 25) {
268
+ // busy-wait sleep for sync lock retries
269
+ }
270
+ }
271
+ }
272
+
273
+ logger.debug('Timed out while waiting for cache lock; skipping cache mutation.')
274
+ return false
275
+ }
276
+
277
+ private async withFileLock<T>(fn: () => Promise<T>): Promise<T | undefined> {
278
+ const locked = await this.acquireLock()
279
+ if (!locked) {
280
+ return undefined
281
+ }
282
+
283
+ try {
284
+ return await fn()
285
+ }
286
+ finally {
287
+ await this.releaseLockSyncOrAsync(false)
288
+ }
289
+ }
290
+
291
+ private withFileLockSync<T>(fn: () => T): T | undefined {
292
+ const locked = this.acquireLockSync()
293
+ if (!locked) {
294
+ return undefined
295
+ }
296
+
297
+ try {
298
+ return fn()
299
+ }
300
+ finally {
301
+ this.releaseLockSyncOrAsync(true)
302
+ }
303
+ }
304
+
305
+ private normalizeContextEntry(value: unknown): CacheIndexEntry | undefined {
306
+ const record = asObject(value)
307
+ if (!record) {
308
+ return undefined
309
+ }
310
+
311
+ const values = toStringArray(record['values'])
312
+ if (values.length === 0) {
313
+ return undefined
314
+ }
315
+
316
+ const contextRecord = asObject(record['context'])
317
+ if (!contextRecord) {
318
+ return undefined
319
+ }
320
+
321
+ const {
322
+ fingerprintVersion,
323
+ projectRootRealpath,
324
+ processCwdRealpath,
325
+ cacheCwdRealpath,
326
+ tailwindConfigPath,
327
+ tailwindConfigMtimeMs,
328
+ tailwindPackageRootRealpath,
329
+ tailwindPackageVersion,
330
+ patcherVersion,
331
+ majorVersion,
332
+ optionsHash,
333
+ } = contextRecord
334
+
335
+ if (
336
+ fingerprintVersion !== 1
337
+ || typeof projectRootRealpath !== 'string'
338
+ || typeof processCwdRealpath !== 'string'
339
+ || typeof cacheCwdRealpath !== 'string'
340
+ || typeof tailwindPackageRootRealpath !== 'string'
341
+ || typeof tailwindPackageVersion !== 'string'
342
+ || typeof patcherVersion !== 'string'
343
+ || (majorVersion !== 2 && majorVersion !== 3 && majorVersion !== 4)
344
+ || typeof optionsHash !== 'string'
345
+ ) {
346
+ return undefined
347
+ }
348
+
349
+ const normalized: CacheIndexEntry = {
350
+ context: {
351
+ fingerprintVersion,
352
+ projectRootRealpath,
353
+ processCwdRealpath,
354
+ cacheCwdRealpath,
355
+ ...(typeof tailwindConfigPath === 'string' ? { tailwindConfigPath } : {}),
356
+ ...(typeof tailwindConfigMtimeMs === 'number' ? { tailwindConfigMtimeMs } : {}),
357
+ tailwindPackageRootRealpath,
358
+ tailwindPackageVersion,
359
+ patcherVersion,
360
+ majorVersion,
361
+ optionsHash,
362
+ },
363
+ values,
364
+ updatedAt: typeof record['updatedAt'] === 'string' ? record['updatedAt'] : new Date(0).toISOString(),
365
+ }
366
+
367
+ return normalized
368
+ }
369
+
370
+ private normalizeIndexFile(payload: unknown): ParsedCacheFile {
371
+ if (Array.isArray(payload)) {
372
+ return {
373
+ kind: 'legacy',
374
+ data: toStringArray(payload),
375
+ }
376
+ }
377
+
378
+ const record = asObject(payload)
379
+ if (!record) {
380
+ return { kind: 'invalid' }
381
+ }
382
+
383
+ if (record['schemaVersion'] !== CACHE_SCHEMA_VERSION) {
384
+ return { kind: 'invalid' }
385
+ }
386
+
387
+ const contextsRecord = asObject(record['contexts'])
388
+ if (!contextsRecord) {
389
+ return { kind: 'invalid' }
390
+ }
391
+
392
+ const contexts: CacheIndexFileV2['contexts'] = {}
393
+
394
+ for (const [fingerprint, value] of Object.entries(contextsRecord)) {
395
+ if (typeof fingerprint !== 'string' || !fingerprint) {
396
+ continue
397
+ }
398
+ const entry = this.normalizeContextEntry(value)
399
+ if (!entry) {
400
+ continue
401
+ }
402
+ contexts[fingerprint] = entry
403
+ }
404
+
405
+ return {
406
+ kind: 'v2',
407
+ data: {
408
+ schemaVersion: CACHE_SCHEMA_VERSION,
409
+ updatedAt: typeof record['updatedAt'] === 'string' ? record['updatedAt'] : new Date(0).toISOString(),
410
+ contexts,
411
+ },
412
+ }
413
+ }
414
+
415
+ private async readParsedCacheFile(cleanupInvalid: boolean): Promise<ParsedCacheFile> {
416
+ try {
417
+ if (!(await fs.pathExists(this.options.path))) {
418
+ return { kind: 'empty' }
419
+ }
420
+
421
+ const payload = await fs.readJSON(this.options.path)
422
+ const normalized = this.normalizeIndexFile(payload)
423
+ if (normalized.kind !== 'invalid') {
424
+ return normalized
425
+ }
426
+
427
+ if (cleanupInvalid) {
428
+ logger.warn('Unable to read Tailwind class cache index, removing invalid file.')
429
+ await fs.remove(this.options.path)
430
+ }
431
+ return { kind: 'invalid' }
432
+ }
433
+ catch (error) {
434
+ if (isErrnoException(error) && error.code === 'ENOENT') {
435
+ return { kind: 'empty' }
436
+ }
437
+
438
+ logger.warn('Unable to read Tailwind class cache index, removing invalid file.', error)
439
+ if (cleanupInvalid) {
440
+ try {
441
+ await fs.remove(this.options.path)
442
+ }
443
+ catch (cleanupError) {
444
+ logger.error('Failed to clean up invalid cache file', cleanupError)
445
+ }
446
+ }
447
+
448
+ return { kind: 'invalid' }
449
+ }
450
+ }
451
+
452
+ private readParsedCacheFileSync(cleanupInvalid: boolean): ParsedCacheFile {
453
+ try {
454
+ if (!fs.pathExistsSync(this.options.path)) {
455
+ return { kind: 'empty' }
456
+ }
457
+
458
+ const payload = fs.readJSONSync(this.options.path)
459
+ const normalized = this.normalizeIndexFile(payload)
460
+ if (normalized.kind !== 'invalid') {
461
+ return normalized
462
+ }
463
+
464
+ if (cleanupInvalid) {
465
+ logger.warn('Unable to read Tailwind class cache index, removing invalid file.')
466
+ fs.removeSync(this.options.path)
467
+ }
468
+ return { kind: 'invalid' }
469
+ }
470
+ catch (error) {
471
+ if (isErrnoException(error) && error.code === 'ENOENT') {
472
+ return { kind: 'empty' }
473
+ }
474
+
475
+ logger.warn('Unable to read Tailwind class cache index, removing invalid file.', error)
476
+ if (cleanupInvalid) {
477
+ try {
478
+ fs.removeSync(this.options.path)
479
+ }
480
+ catch (cleanupError) {
481
+ logger.error('Failed to clean up invalid cache file', cleanupError)
482
+ }
483
+ }
484
+
485
+ return { kind: 'invalid' }
486
+ }
487
+ }
488
+
489
+ private findProjectMatch(index: CacheIndexFileV2) {
490
+ if (!this.context) {
491
+ return undefined
492
+ }
493
+
494
+ const current = this.context.metadata.projectRootRealpath
495
+ return Object.entries(index.contexts).find(([, entry]) => entry.context.projectRootRealpath === current)
496
+ }
497
+
498
+ private async writeIndexFile(index: CacheIndexFileV2): Promise<string | undefined> {
499
+ const tempPath = this.createTempPath()
500
+
501
+ try {
502
+ await this.ensureDir()
503
+ await fs.writeJSON(tempPath, index)
504
+ const replaced = await this.replaceCacheFile(tempPath)
505
+ if (replaced) {
506
+ return this.options.path
507
+ }
508
+
509
+ await this.cleanupTempFile(tempPath)
510
+ return undefined
511
+ }
512
+ catch (error) {
513
+ await this.cleanupTempFile(tempPath)
514
+ logger.error('Unable to persist Tailwind class cache', error)
515
+ return undefined
516
+ }
517
+ }
518
+
519
+ private writeIndexFileSync(index: CacheIndexFileV2): string | undefined {
520
+ const tempPath = this.createTempPath()
521
+
522
+ try {
523
+ this.ensureDirSync()
524
+ fs.writeJSONSync(tempPath, index)
525
+ const replaced = this.replaceCacheFileSync(tempPath)
526
+ if (replaced) {
527
+ return this.options.path
528
+ }
529
+
530
+ this.cleanupTempFileSync(tempPath)
531
+ return undefined
532
+ }
533
+ catch (error) {
534
+ this.cleanupTempFileSync(tempPath)
535
+ logger.error('Unable to persist Tailwind class cache', error)
536
+ return undefined
537
+ }
538
+ }
539
+
540
+ async write(data: Set<string>): Promise<string | undefined> {
541
+ if (!this.options.enabled) {
542
+ return undefined
543
+ }
544
+
545
+ if (this.driver === 'noop') {
546
+ return undefined
547
+ }
548
+
549
+ if (this.driver === 'memory') {
550
+ if (!this.isContextAware()) {
551
+ this.memoryCache = new Set(data)
552
+ return 'memory'
553
+ }
554
+
555
+ const index = this.memoryIndex ?? this.createEmptyIndex()
556
+ if (!this.context) {
557
+ return 'memory'
558
+ }
559
+ index.contexts[this.context.fingerprint] = {
560
+ context: {
561
+ ...this.context.metadata,
562
+ },
563
+ values: Array.from(data),
564
+ updatedAt: new Date().toISOString(),
565
+ }
566
+ index.updatedAt = new Date().toISOString()
567
+ this.memoryIndex = index
568
+ return 'memory'
569
+ }
570
+
571
+ if (!this.isContextAware()) {
572
+ const tempPath = this.createTempPath()
573
+ try {
574
+ await this.ensureDir()
575
+ await fs.writeJSON(tempPath, Array.from(data))
576
+ const replaced = await this.replaceCacheFile(tempPath)
577
+ if (replaced) {
578
+ return this.options.path
579
+ }
580
+
581
+ await this.cleanupTempFile(tempPath)
582
+ return undefined
583
+ }
584
+ catch (error) {
585
+ await this.cleanupTempFile(tempPath)
586
+ logger.error('Unable to persist Tailwind class cache', error)
587
+ return undefined
588
+ }
589
+ }
590
+
591
+ const result = await this.withFileLock(async () => {
592
+ const parsed = await this.readParsedCacheFile(false)
593
+ const index = parsed.kind === 'v2' ? parsed.data : this.createEmptyIndex()
594
+
595
+ if (this.context) {
596
+ index.contexts[this.context.fingerprint] = {
597
+ context: {
598
+ ...this.context.metadata,
599
+ },
600
+ values: Array.from(data),
601
+ updatedAt: new Date().toISOString(),
602
+ }
603
+ }
604
+
605
+ index.updatedAt = new Date().toISOString()
606
+ return this.writeIndexFile(index)
607
+ })
608
+
609
+ return result
610
+ }
611
+
612
+ writeSync(data: Set<string>): string | undefined {
613
+ if (!this.options.enabled) {
614
+ return undefined
615
+ }
616
+
617
+ if (this.driver === 'noop') {
618
+ return undefined
619
+ }
620
+
621
+ if (this.driver === 'memory') {
622
+ if (!this.isContextAware()) {
623
+ this.memoryCache = new Set(data)
624
+ return 'memory'
625
+ }
626
+
627
+ const index = this.memoryIndex ?? this.createEmptyIndex()
628
+ if (!this.context) {
629
+ return 'memory'
630
+ }
631
+ index.contexts[this.context.fingerprint] = {
632
+ context: {
633
+ ...this.context.metadata,
634
+ },
635
+ values: Array.from(data),
636
+ updatedAt: new Date().toISOString(),
637
+ }
638
+ index.updatedAt = new Date().toISOString()
639
+ this.memoryIndex = index
640
+ return 'memory'
641
+ }
642
+
643
+ if (!this.isContextAware()) {
644
+ const tempPath = this.createTempPath()
645
+ try {
646
+ this.ensureDirSync()
647
+ fs.writeJSONSync(tempPath, Array.from(data))
648
+ const replaced = this.replaceCacheFileSync(tempPath)
649
+ if (replaced) {
650
+ return this.options.path
651
+ }
652
+
653
+ this.cleanupTempFileSync(tempPath)
654
+ return undefined
655
+ }
656
+ catch (error) {
657
+ this.cleanupTempFileSync(tempPath)
658
+ logger.error('Unable to persist Tailwind class cache', error)
659
+ return undefined
660
+ }
661
+ }
662
+
663
+ const result = this.withFileLockSync(() => {
664
+ const parsed = this.readParsedCacheFileSync(false)
665
+ const index = parsed.kind === 'v2' ? parsed.data : this.createEmptyIndex()
666
+
667
+ if (this.context) {
668
+ index.contexts[this.context.fingerprint] = {
669
+ context: {
670
+ ...this.context.metadata,
671
+ },
672
+ values: Array.from(data),
673
+ updatedAt: new Date().toISOString(),
674
+ }
675
+ }
676
+
677
+ index.updatedAt = new Date().toISOString()
678
+ return this.writeIndexFileSync(index)
679
+ })
680
+
681
+ return result
682
+ }
683
+
684
+ async readWithMeta(): Promise<CacheReadResult> {
685
+ if (!this.options.enabled) {
686
+ return {
687
+ data: new Set(),
688
+ meta: {
689
+ hit: false,
690
+ reason: 'cache-disabled',
691
+ details: ['cache disabled'],
692
+ },
693
+ }
694
+ }
695
+
696
+ if (this.driver === 'noop') {
697
+ return {
698
+ data: new Set(),
699
+ meta: {
700
+ hit: false,
701
+ reason: 'noop-driver',
702
+ details: ['cache driver is noop'],
703
+ },
704
+ }
705
+ }
706
+
707
+ if (this.driver === 'memory') {
708
+ if (!this.isContextAware()) {
709
+ const cache = new Set(this.memoryCache ?? [])
710
+ return {
711
+ data: cache,
712
+ meta: {
713
+ hit: cache.size > 0,
714
+ reason: cache.size > 0 ? 'hit' : 'context-not-found',
715
+ details: cache.size > 0 ? ['memory cache hit'] : ['memory cache miss'],
716
+ },
717
+ }
718
+ }
719
+
720
+ const index = this.memoryIndex
721
+ if (!index || !this.context) {
722
+ return {
723
+ data: new Set(),
724
+ meta: {
725
+ hit: false,
726
+ reason: 'context-not-found',
727
+ ...(this.context?.fingerprint === undefined ? {} : { fingerprint: this.context.fingerprint }),
728
+ schemaVersion: CACHE_SCHEMA_VERSION,
729
+ details: ['no in-memory cache index for current context'],
730
+ },
731
+ }
732
+ }
733
+
734
+ const entry = index.contexts[this.context.fingerprint]
735
+ if (entry) {
736
+ return {
737
+ data: new Set(entry.values),
738
+ meta: {
739
+ hit: true,
740
+ reason: 'hit',
741
+ fingerprint: this.context.fingerprint,
742
+ schemaVersion: CACHE_SCHEMA_VERSION,
743
+ details: ['memory cache hit'],
744
+ },
745
+ }
746
+ }
747
+
748
+ const projectMatch = this.findProjectMatch(index)
749
+ if (projectMatch && this.context) {
750
+ const [, matchedEntry] = projectMatch
751
+ return {
752
+ data: new Set(),
753
+ meta: {
754
+ hit: false,
755
+ reason: 'context-mismatch',
756
+ fingerprint: this.context.fingerprint,
757
+ schemaVersion: CACHE_SCHEMA_VERSION,
758
+ details: explainContextMismatch(this.context.metadata, matchedEntry.context),
759
+ },
760
+ }
761
+ }
762
+
763
+ return {
764
+ data: new Set(),
765
+ meta: {
766
+ hit: false,
767
+ reason: 'context-not-found',
768
+ fingerprint: this.context.fingerprint,
769
+ schemaVersion: CACHE_SCHEMA_VERSION,
770
+ details: ['context fingerprint not found in memory cache index'],
771
+ },
772
+ }
773
+ }
774
+
775
+ const parsed = await this.readParsedCacheFile(true)
776
+
777
+ if (parsed.kind === 'empty') {
778
+ return {
779
+ data: new Set(),
780
+ meta: {
781
+ hit: false,
782
+ reason: 'file-missing',
783
+ details: ['cache file not found'],
784
+ },
785
+ }
786
+ }
787
+
788
+ if (parsed.kind === 'invalid') {
789
+ return {
790
+ data: new Set(),
791
+ meta: {
792
+ hit: false,
793
+ reason: 'invalid-schema',
794
+ details: ['cache schema invalid and has been reset'],
795
+ },
796
+ }
797
+ }
798
+
799
+ if (!this.isContextAware()) {
800
+ if (parsed.kind === 'legacy') {
801
+ return {
802
+ data: new Set(parsed.data),
803
+ meta: {
804
+ hit: parsed.data.length > 0,
805
+ reason: parsed.data.length > 0 ? 'hit' : 'context-not-found',
806
+ details: ['legacy cache format'],
807
+ },
808
+ }
809
+ }
810
+
811
+ const union = Object.values(parsed.data.contexts).flatMap(entry => entry.values)
812
+ return {
813
+ data: new Set(union),
814
+ meta: {
815
+ hit: union.length > 0,
816
+ reason: union.length > 0 ? 'hit' : 'context-not-found',
817
+ schemaVersion: parsed.data.schemaVersion,
818
+ details: ['context-less read merged all cache entries'],
819
+ },
820
+ }
821
+ }
822
+
823
+ if (parsed.kind === 'legacy') {
824
+ return {
825
+ data: new Set(),
826
+ meta: {
827
+ hit: false,
828
+ reason: 'legacy-schema',
829
+ ...(this.context?.fingerprint === undefined ? {} : { fingerprint: this.context.fingerprint }),
830
+ details: ['legacy cache schema detected; rebuilding cache with context fingerprint'],
831
+ },
832
+ }
833
+ }
834
+
835
+ if (!this.context) {
836
+ return {
837
+ data: new Set(),
838
+ meta: {
839
+ hit: false,
840
+ reason: 'context-not-found',
841
+ details: ['cache context missing'],
842
+ },
843
+ }
844
+ }
845
+
846
+ const entry = parsed.data.contexts[this.context.fingerprint]
847
+ if (entry) {
848
+ const mismatchReasons = explainContextMismatch(this.context.metadata, entry.context)
849
+ if (mismatchReasons.length === 0) {
850
+ return {
851
+ data: new Set(entry.values),
852
+ meta: {
853
+ hit: true,
854
+ reason: 'hit',
855
+ fingerprint: this.context.fingerprint,
856
+ schemaVersion: parsed.data.schemaVersion,
857
+ details: [`context fingerprint ${this.context.fingerprint.slice(0, 12)} matched`],
858
+ },
859
+ }
860
+ }
861
+
862
+ return {
863
+ data: new Set(),
864
+ meta: {
865
+ hit: false,
866
+ reason: 'context-mismatch',
867
+ fingerprint: this.context.fingerprint,
868
+ schemaVersion: parsed.data.schemaVersion,
869
+ details: mismatchReasons,
870
+ },
871
+ }
872
+ }
873
+
874
+ const projectMatch = this.findProjectMatch(parsed.data)
875
+ if (projectMatch) {
876
+ const [matchedFingerprint, matchedEntry] = projectMatch
877
+ return {
878
+ data: new Set(),
879
+ meta: {
880
+ hit: false,
881
+ reason: 'context-mismatch',
882
+ fingerprint: this.context.fingerprint,
883
+ schemaVersion: parsed.data.schemaVersion,
884
+ details: [
885
+ `nearest context fingerprint: ${matchedFingerprint.slice(0, 12)}`,
886
+ ...explainContextMismatch(this.context.metadata, matchedEntry.context),
887
+ ],
888
+ },
889
+ }
890
+ }
891
+
892
+ return {
893
+ data: new Set(),
894
+ meta: {
895
+ hit: false,
896
+ reason: 'context-not-found',
897
+ fingerprint: this.context.fingerprint,
898
+ schemaVersion: parsed.data.schemaVersion,
899
+ details: ['context fingerprint not found in cache index'],
900
+ },
901
+ }
902
+ }
903
+
904
+ readWithMetaSync(): CacheReadResult {
905
+ if (!this.options.enabled) {
906
+ return {
907
+ data: new Set(),
908
+ meta: {
909
+ hit: false,
910
+ reason: 'cache-disabled',
911
+ details: ['cache disabled'],
912
+ },
913
+ }
914
+ }
915
+
916
+ if (this.driver === 'noop') {
917
+ return {
918
+ data: new Set(),
919
+ meta: {
920
+ hit: false,
921
+ reason: 'noop-driver',
922
+ details: ['cache driver is noop'],
923
+ },
924
+ }
925
+ }
926
+
927
+ if (this.driver === 'memory') {
928
+ if (!this.isContextAware()) {
929
+ const cache = new Set(this.memoryCache ?? [])
930
+ return {
931
+ data: cache,
932
+ meta: {
933
+ hit: cache.size > 0,
934
+ reason: cache.size > 0 ? 'hit' : 'context-not-found',
935
+ details: cache.size > 0 ? ['memory cache hit'] : ['memory cache miss'],
936
+ },
937
+ }
938
+ }
939
+
940
+ const index = this.memoryIndex
941
+ if (!index || !this.context) {
942
+ return {
943
+ data: new Set(),
944
+ meta: {
945
+ hit: false,
946
+ reason: 'context-not-found',
947
+ ...(this.context?.fingerprint === undefined ? {} : { fingerprint: this.context.fingerprint }),
948
+ schemaVersion: CACHE_SCHEMA_VERSION,
949
+ details: ['no in-memory cache index for current context'],
950
+ },
951
+ }
952
+ }
953
+
954
+ const entry = index.contexts[this.context.fingerprint]
955
+ if (entry) {
956
+ return {
957
+ data: new Set(entry.values),
958
+ meta: {
959
+ hit: true,
960
+ reason: 'hit',
961
+ fingerprint: this.context.fingerprint,
962
+ schemaVersion: CACHE_SCHEMA_VERSION,
963
+ details: ['memory cache hit'],
964
+ },
965
+ }
966
+ }
967
+
968
+ const projectMatch = this.findProjectMatch(index)
969
+ if (projectMatch && this.context) {
970
+ const [, matchedEntry] = projectMatch
971
+ return {
972
+ data: new Set(),
973
+ meta: {
974
+ hit: false,
975
+ reason: 'context-mismatch',
976
+ fingerprint: this.context.fingerprint,
977
+ schemaVersion: CACHE_SCHEMA_VERSION,
978
+ details: explainContextMismatch(this.context.metadata, matchedEntry.context),
979
+ },
980
+ }
981
+ }
982
+
983
+ return {
984
+ data: new Set(),
985
+ meta: {
986
+ hit: false,
987
+ reason: 'context-not-found',
988
+ fingerprint: this.context.fingerprint,
989
+ schemaVersion: CACHE_SCHEMA_VERSION,
990
+ details: ['context fingerprint not found in memory cache index'],
991
+ },
992
+ }
993
+ }
994
+
995
+ const parsed = this.readParsedCacheFileSync(true)
996
+
997
+ if (parsed.kind === 'empty') {
998
+ return {
999
+ data: new Set(),
1000
+ meta: {
1001
+ hit: false,
1002
+ reason: 'file-missing',
1003
+ details: ['cache file not found'],
1004
+ },
1005
+ }
1006
+ }
1007
+
1008
+ if (parsed.kind === 'invalid') {
1009
+ return {
1010
+ data: new Set(),
1011
+ meta: {
1012
+ hit: false,
1013
+ reason: 'invalid-schema',
1014
+ details: ['cache schema invalid and has been reset'],
1015
+ },
1016
+ }
1017
+ }
1018
+
1019
+ if (!this.isContextAware()) {
1020
+ if (parsed.kind === 'legacy') {
1021
+ return {
1022
+ data: new Set(parsed.data),
1023
+ meta: {
1024
+ hit: parsed.data.length > 0,
1025
+ reason: parsed.data.length > 0 ? 'hit' : 'context-not-found',
1026
+ details: ['legacy cache format'],
1027
+ },
1028
+ }
1029
+ }
1030
+
1031
+ const union = Object.values(parsed.data.contexts).flatMap(entry => entry.values)
1032
+ return {
1033
+ data: new Set(union),
1034
+ meta: {
1035
+ hit: union.length > 0,
1036
+ reason: union.length > 0 ? 'hit' : 'context-not-found',
1037
+ schemaVersion: parsed.data.schemaVersion,
1038
+ details: ['context-less read merged all cache entries'],
1039
+ },
1040
+ }
1041
+ }
1042
+
1043
+ if (parsed.kind === 'legacy') {
1044
+ return {
1045
+ data: new Set(),
1046
+ meta: {
1047
+ hit: false,
1048
+ reason: 'legacy-schema',
1049
+ ...(this.context?.fingerprint === undefined ? {} : { fingerprint: this.context.fingerprint }),
1050
+ details: ['legacy cache schema detected; rebuilding cache with context fingerprint'],
1051
+ },
1052
+ }
1053
+ }
1054
+
1055
+ if (!this.context) {
1056
+ return {
1057
+ data: new Set(),
1058
+ meta: {
1059
+ hit: false,
1060
+ reason: 'context-not-found',
1061
+ details: ['cache context missing'],
1062
+ },
1063
+ }
1064
+ }
1065
+
1066
+ const entry = parsed.data.contexts[this.context.fingerprint]
1067
+ if (entry) {
1068
+ const mismatchReasons = explainContextMismatch(this.context.metadata, entry.context)
1069
+ if (mismatchReasons.length === 0) {
1070
+ return {
1071
+ data: new Set(entry.values),
1072
+ meta: {
1073
+ hit: true,
1074
+ reason: 'hit',
1075
+ fingerprint: this.context.fingerprint,
1076
+ schemaVersion: parsed.data.schemaVersion,
1077
+ details: [`context fingerprint ${this.context.fingerprint.slice(0, 12)} matched`],
1078
+ },
1079
+ }
1080
+ }
1081
+
1082
+ return {
1083
+ data: new Set(),
1084
+ meta: {
1085
+ hit: false,
1086
+ reason: 'context-mismatch',
1087
+ fingerprint: this.context.fingerprint,
1088
+ schemaVersion: parsed.data.schemaVersion,
1089
+ details: mismatchReasons,
1090
+ },
1091
+ }
1092
+ }
1093
+
1094
+ const projectMatch = this.findProjectMatch(parsed.data)
1095
+ if (projectMatch) {
1096
+ const [matchedFingerprint, matchedEntry] = projectMatch
1097
+ return {
1098
+ data: new Set(),
1099
+ meta: {
1100
+ hit: false,
1101
+ reason: 'context-mismatch',
1102
+ fingerprint: this.context.fingerprint,
1103
+ schemaVersion: parsed.data.schemaVersion,
1104
+ details: [
1105
+ `nearest context fingerprint: ${matchedFingerprint.slice(0, 12)}`,
1106
+ ...explainContextMismatch(this.context.metadata, matchedEntry.context),
1107
+ ],
1108
+ },
1109
+ }
1110
+ }
1111
+
1112
+ return {
1113
+ data: new Set(),
1114
+ meta: {
1115
+ hit: false,
1116
+ reason: 'context-not-found',
1117
+ fingerprint: this.context.fingerprint,
1118
+ schemaVersion: parsed.data.schemaVersion,
1119
+ details: ['context fingerprint not found in cache index'],
1120
+ },
1121
+ }
1122
+ }
1123
+
1124
+ async read(): Promise<Set<string>> {
1125
+ const result = await this.readWithMeta()
1126
+ this.lastReadMeta = toReadMeta(result.meta)
1127
+ return new Set(result.data)
1128
+ }
1129
+
1130
+ readSync(): Set<string> {
1131
+ const result = this.readWithMetaSync()
1132
+ this.lastReadMeta = toReadMeta(result.meta)
1133
+ return new Set(result.data)
1134
+ }
1135
+
1136
+ getLastReadMeta(): CacheReadMeta {
1137
+ return toReadMeta(this.lastReadMeta)
1138
+ }
1139
+
1140
+ private countEntriesFromParsed(parsed: ParsedCacheFile): { contexts: number, entries: number } {
1141
+ if (parsed.kind === 'legacy') {
1142
+ return {
1143
+ contexts: parsed.data.length ? 1 : 0,
1144
+ entries: parsed.data.length,
1145
+ }
1146
+ }
1147
+
1148
+ if (parsed.kind === 'v2') {
1149
+ const values = Object.values(parsed.data.contexts)
1150
+ return {
1151
+ contexts: values.length,
1152
+ entries: values.reduce((acc, item) => acc + item.values.length, 0),
1153
+ }
1154
+ }
1155
+
1156
+ return {
1157
+ contexts: 0,
1158
+ entries: 0,
1159
+ }
1160
+ }
1161
+
1162
+ async clear(options?: CacheClearOptions): Promise<CacheClearResult> {
1163
+ const scope = options?.scope ?? 'current'
1164
+
1165
+ if (!this.options.enabled || this.driver === 'noop') {
1166
+ return {
1167
+ scope,
1168
+ filesRemoved: 0,
1169
+ entriesRemoved: 0,
1170
+ contextsRemoved: 0,
1171
+ }
1172
+ }
1173
+
1174
+ if (this.driver === 'memory') {
1175
+ if (!this.isContextAware() || scope === 'all') {
1176
+ const entriesRemoved = this.memoryCache?.size ?? (this.memoryIndex ? this.countEntriesFromParsed({ kind: 'v2', data: this.memoryIndex }).entries : 0)
1177
+ const contextsRemoved = this.memoryIndex ? Object.keys(this.memoryIndex.contexts).length : (this.memoryCache?.size ? 1 : 0)
1178
+ this.memoryCache = null
1179
+ this.memoryIndex = null
1180
+ return {
1181
+ scope,
1182
+ filesRemoved: 0,
1183
+ entriesRemoved,
1184
+ contextsRemoved,
1185
+ }
1186
+ }
1187
+
1188
+ if (!this.context || !this.memoryIndex) {
1189
+ return {
1190
+ scope,
1191
+ filesRemoved: 0,
1192
+ entriesRemoved: 0,
1193
+ contextsRemoved: 0,
1194
+ }
1195
+ }
1196
+
1197
+ const entry = this.memoryIndex.contexts[this.context.fingerprint]
1198
+ if (!entry) {
1199
+ return {
1200
+ scope,
1201
+ filesRemoved: 0,
1202
+ entriesRemoved: 0,
1203
+ contextsRemoved: 0,
1204
+ }
1205
+ }
1206
+
1207
+ const entriesRemoved = entry.values.length
1208
+ delete this.memoryIndex.contexts[this.context.fingerprint]
1209
+ return {
1210
+ scope,
1211
+ filesRemoved: 0,
1212
+ entriesRemoved,
1213
+ contextsRemoved: 1,
1214
+ }
1215
+ }
1216
+
1217
+ const result = await this.withFileLock(async () => {
1218
+ const parsed = await this.readParsedCacheFile(false)
1219
+ if (parsed.kind === 'empty') {
1220
+ return {
1221
+ scope,
1222
+ filesRemoved: 0,
1223
+ entriesRemoved: 0,
1224
+ contextsRemoved: 0,
1225
+ }
1226
+ }
1227
+
1228
+ if (!this.isContextAware() || scope === 'all') {
1229
+ const counts = this.countEntriesFromParsed(parsed)
1230
+ await fs.remove(this.options.path)
1231
+ return {
1232
+ scope,
1233
+ filesRemoved: 1,
1234
+ entriesRemoved: counts.entries,
1235
+ contextsRemoved: counts.contexts,
1236
+ }
1237
+ }
1238
+
1239
+ if (parsed.kind !== 'v2' || !this.context) {
1240
+ const counts = this.countEntriesFromParsed(parsed)
1241
+ await fs.remove(this.options.path)
1242
+ return {
1243
+ scope,
1244
+ filesRemoved: 1,
1245
+ entriesRemoved: counts.entries,
1246
+ contextsRemoved: counts.contexts,
1247
+ }
1248
+ }
1249
+
1250
+ const entry = parsed.data.contexts[this.context.fingerprint]
1251
+ if (!entry) {
1252
+ return {
1253
+ scope,
1254
+ filesRemoved: 0,
1255
+ entriesRemoved: 0,
1256
+ contextsRemoved: 0,
1257
+ }
1258
+ }
1259
+
1260
+ const entriesRemoved = entry.values.length
1261
+ delete parsed.data.contexts[this.context.fingerprint]
1262
+ const remain = Object.keys(parsed.data.contexts).length
1263
+ if (remain === 0) {
1264
+ await fs.remove(this.options.path)
1265
+ return {
1266
+ scope,
1267
+ filesRemoved: 1,
1268
+ entriesRemoved,
1269
+ contextsRemoved: 1,
1270
+ }
1271
+ }
1272
+
1273
+ parsed.data.updatedAt = new Date().toISOString()
1274
+ await this.writeIndexFile(parsed.data)
1275
+ return {
1276
+ scope,
1277
+ filesRemoved: 0,
1278
+ entriesRemoved,
1279
+ contextsRemoved: 1,
1280
+ }
1281
+ })
1282
+
1283
+ return result ?? {
1284
+ scope,
1285
+ filesRemoved: 0,
1286
+ entriesRemoved: 0,
1287
+ contextsRemoved: 0,
1288
+ }
1289
+ }
1290
+
1291
+ clearSync(options?: CacheClearOptions): CacheClearResult {
1292
+ const scope = options?.scope ?? 'current'
1293
+
1294
+ if (!this.options.enabled || this.driver === 'noop') {
1295
+ return {
1296
+ scope,
1297
+ filesRemoved: 0,
1298
+ entriesRemoved: 0,
1299
+ contextsRemoved: 0,
1300
+ }
1301
+ }
1302
+
1303
+ if (this.driver === 'memory') {
1304
+ if (!this.isContextAware() || scope === 'all') {
1305
+ const entriesRemoved = this.memoryCache?.size ?? (this.memoryIndex ? this.countEntriesFromParsed({ kind: 'v2', data: this.memoryIndex }).entries : 0)
1306
+ const contextsRemoved = this.memoryIndex ? Object.keys(this.memoryIndex.contexts).length : (this.memoryCache?.size ? 1 : 0)
1307
+ this.memoryCache = null
1308
+ this.memoryIndex = null
1309
+ return {
1310
+ scope,
1311
+ filesRemoved: 0,
1312
+ entriesRemoved,
1313
+ contextsRemoved,
1314
+ }
1315
+ }
1316
+
1317
+ if (!this.context || !this.memoryIndex) {
1318
+ return {
1319
+ scope,
1320
+ filesRemoved: 0,
1321
+ entriesRemoved: 0,
1322
+ contextsRemoved: 0,
1323
+ }
1324
+ }
1325
+
1326
+ const entry = this.memoryIndex.contexts[this.context.fingerprint]
1327
+ if (!entry) {
1328
+ return {
1329
+ scope,
1330
+ filesRemoved: 0,
1331
+ entriesRemoved: 0,
1332
+ contextsRemoved: 0,
1333
+ }
1334
+ }
1335
+
1336
+ const entriesRemoved = entry.values.length
1337
+ delete this.memoryIndex.contexts[this.context.fingerprint]
1338
+ return {
1339
+ scope,
1340
+ filesRemoved: 0,
1341
+ entriesRemoved,
1342
+ contextsRemoved: 1,
1343
+ }
1344
+ }
1345
+
1346
+ const result = this.withFileLockSync(() => {
1347
+ const parsed = this.readParsedCacheFileSync(false)
1348
+ if (parsed.kind === 'empty') {
1349
+ return {
1350
+ scope,
1351
+ filesRemoved: 0,
1352
+ entriesRemoved: 0,
1353
+ contextsRemoved: 0,
1354
+ }
1355
+ }
1356
+
1357
+ if (!this.isContextAware() || scope === 'all') {
1358
+ const counts = this.countEntriesFromParsed(parsed)
1359
+ fs.removeSync(this.options.path)
1360
+ return {
1361
+ scope,
1362
+ filesRemoved: 1,
1363
+ entriesRemoved: counts.entries,
1364
+ contextsRemoved: counts.contexts,
1365
+ }
1366
+ }
1367
+
1368
+ if (parsed.kind !== 'v2' || !this.context) {
1369
+ const counts = this.countEntriesFromParsed(parsed)
1370
+ fs.removeSync(this.options.path)
1371
+ return {
1372
+ scope,
1373
+ filesRemoved: 1,
1374
+ entriesRemoved: counts.entries,
1375
+ contextsRemoved: counts.contexts,
1376
+ }
1377
+ }
1378
+
1379
+ const entry = parsed.data.contexts[this.context.fingerprint]
1380
+ if (!entry) {
1381
+ return {
1382
+ scope,
1383
+ filesRemoved: 0,
1384
+ entriesRemoved: 0,
1385
+ contextsRemoved: 0,
1386
+ }
1387
+ }
1388
+
1389
+ const entriesRemoved = entry.values.length
1390
+ delete parsed.data.contexts[this.context.fingerprint]
1391
+ const remain = Object.keys(parsed.data.contexts).length
1392
+ if (remain === 0) {
1393
+ fs.removeSync(this.options.path)
1394
+ return {
1395
+ scope,
1396
+ filesRemoved: 1,
1397
+ entriesRemoved,
1398
+ contextsRemoved: 1,
1399
+ }
1400
+ }
1401
+
1402
+ parsed.data.updatedAt = new Date().toISOString()
1403
+ this.writeIndexFileSync(parsed.data)
1404
+ return {
1405
+ scope,
1406
+ filesRemoved: 0,
1407
+ entriesRemoved,
1408
+ contextsRemoved: 1,
1409
+ }
1410
+ })
1411
+
1412
+ return result ?? {
1413
+ scope,
1414
+ filesRemoved: 0,
1415
+ entriesRemoved: 0,
1416
+ contextsRemoved: 0,
1417
+ }
1418
+ }
1419
+
1420
+ readIndexSnapshot(): CacheIndexFileV2 | undefined {
1421
+ if (this.driver === 'memory') {
1422
+ return this.memoryIndex
1423
+ ? {
1424
+ ...this.memoryIndex,
1425
+ contexts: Object.fromEntries(Object.entries(this.memoryIndex.contexts).map(([key, value]) => [key, cloneEntry(value)])),
1426
+ }
1427
+ : undefined
1428
+ }
1429
+
1430
+ const parsed = this.readParsedCacheFileSync(false)
1431
+ if (parsed.kind !== 'v2') {
1432
+ return undefined
1433
+ }
1434
+
1435
+ return {
1436
+ ...parsed.data,
1437
+ contexts: Object.fromEntries(Object.entries(parsed.data.contexts).map(([key, value]) => [key, cloneEntry(value)])),
1438
+ }
1439
+ }
1440
+ }