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.
- package/README.md +20 -0
- package/dist/{chunk-Z6OMJZTU.js → chunk-TOAZIPHJ.js} +29 -11
- package/dist/{chunk-SWLOK2S6.mjs → chunk-VDWTCQ74.mjs} +29 -11
- package/dist/cli.js +4 -4
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +43 -35
- package/dist/index.d.ts +43 -35
- package/dist/index.js +2 -2
- package/dist/index.mjs +1 -1
- package/package.json +8 -3
- package/src/api/tailwindcss-patcher.ts +424 -0
- package/src/babel/index.ts +12 -0
- package/src/cache/context.ts +212 -0
- package/src/cache/store.ts +1440 -0
- package/src/cache/types.ts +71 -0
- package/src/cli.ts +20 -0
- package/src/commands/basic-handlers.ts +145 -0
- package/src/commands/cli.ts +56 -0
- package/src/commands/command-context.ts +77 -0
- package/src/commands/command-definitions.ts +102 -0
- package/src/commands/command-metadata.ts +68 -0
- package/src/commands/command-registrar.ts +39 -0
- package/src/commands/command-runtime.ts +33 -0
- package/src/commands/default-handler-map.ts +25 -0
- package/src/commands/migrate-config.ts +104 -0
- package/src/commands/migrate-handler.ts +67 -0
- package/src/commands/migration-aggregation.ts +100 -0
- package/src/commands/migration-args.ts +85 -0
- package/src/commands/migration-file-executor.ts +189 -0
- package/src/commands/migration-output.ts +115 -0
- package/src/commands/migration-report-loader.ts +26 -0
- package/src/commands/migration-report.ts +21 -0
- package/src/commands/migration-source.ts +318 -0
- package/src/commands/migration-target-files.ts +161 -0
- package/src/commands/migration-target-resolver.ts +34 -0
- package/src/commands/migration-types.ts +65 -0
- package/src/commands/restore-handler.ts +24 -0
- package/src/commands/status-handler.ts +17 -0
- package/src/commands/status-output.ts +60 -0
- package/src/commands/token-output.ts +30 -0
- package/src/commands/types.ts +137 -0
- package/src/commands/validate-handler.ts +42 -0
- package/src/commands/validate.ts +83 -0
- package/src/config/index.ts +25 -0
- package/src/config/workspace.ts +87 -0
- package/src/constants.ts +4 -0
- package/src/extraction/candidate-extractor.ts +354 -0
- package/src/index.ts +57 -0
- package/src/install/class-collector.ts +1 -0
- package/src/install/context-registry.ts +1 -0
- package/src/install/index.ts +5 -0
- package/src/install/patch-runner.ts +1 -0
- package/src/install/process-tailwindcss.ts +1 -0
- package/src/install/status.ts +1 -0
- package/src/logger.ts +5 -0
- package/src/options/legacy.ts +93 -0
- package/src/options/normalize.ts +262 -0
- package/src/options/types.ts +217 -0
- package/src/patching/operations/export-context/index.ts +110 -0
- package/src/patching/operations/export-context/postcss-v2.ts +235 -0
- package/src/patching/operations/export-context/postcss-v3.ts +249 -0
- package/src/patching/operations/extend-length-units.ts +197 -0
- package/src/patching/patch-runner.ts +46 -0
- package/src/patching/status.ts +262 -0
- package/src/runtime/class-collector.ts +105 -0
- package/src/runtime/collector.ts +148 -0
- package/src/runtime/context-registry.ts +65 -0
- package/src/runtime/process-tailwindcss.ts +115 -0
- package/src/types.ts +159 -0
- 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
|
+
}
|