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,424 @@
1
+ import type { SourceEntry } from '@tailwindcss/oxide'
2
+ import type { PackageInfo } from 'local-pkg'
3
+ import type { NormalizedTailwindCssPatchOptions } from '../config'
4
+ import type {
5
+ CacheClearOptions,
6
+ CacheClearResult,
7
+ CacheContextMetadata,
8
+ CacheReadMeta,
9
+ ExtractResult,
10
+ TailwindCssPatchOptions,
11
+ TailwindTokenByFileMap,
12
+ TailwindTokenFileKey,
13
+ TailwindTokenReport,
14
+ } from '../types'
15
+ import process from 'node:process'
16
+ import fs from 'fs-extra'
17
+ import { getPackageInfoSync } from 'local-pkg'
18
+ import path from 'pathe'
19
+ import { coerce } from 'semver'
20
+ import { createCacheContextDescriptor } from '../cache/context'
21
+ import { CacheStore } from '../cache/store'
22
+ import { normalizeOptions } from '../config'
23
+ import {
24
+ extractValidCandidates as extractCandidates,
25
+ extractProjectCandidatesWithPositions,
26
+ groupTokensByFile,
27
+ } from '../extraction/candidate-extractor'
28
+ import { collectClassesFromContexts } from '../install/class-collector'
29
+ import logger from '../logger'
30
+ import { RuntimeCollector, TailwindV4Collector, type PatchResult, type TailwindCollector, type TailwindMajorVersion } from '../runtime/collector'
31
+
32
+
33
+ interface PatchMemo {
34
+ result: PatchResult
35
+ snapshot: string
36
+ }
37
+
38
+ function resolveInstalledMajorVersion(version?: string | null) {
39
+ if (!version) {
40
+ return undefined
41
+ }
42
+
43
+ const coerced = coerce(version)
44
+ if (!coerced) {
45
+ return undefined
46
+ }
47
+
48
+ const major = coerced.major as TailwindMajorVersion
49
+ if (major === 2 || major === 3 || major === 4) {
50
+ return major
51
+ }
52
+
53
+ if (major >= 4) {
54
+ return 4
55
+ }
56
+
57
+ return undefined
58
+ }
59
+
60
+ function validateInstalledVersion(packageVersion: string | undefined, expectedMajor: TailwindMajorVersion, packageName: string) {
61
+ const installedMajor = resolveInstalledMajorVersion(packageVersion)
62
+ if (installedMajor === undefined) {
63
+ return
64
+ }
65
+
66
+ if (installedMajor !== expectedMajor) {
67
+ throw new Error(
68
+ `Configured tailwindcss.version=${expectedMajor}, but resolved package "${packageName}" is version ${packageVersion}. Update the configuration or resolve the correct package.`,
69
+ )
70
+ }
71
+ }
72
+
73
+ function resolveMajorVersionOrThrow(
74
+ configuredMajor: TailwindMajorVersion | undefined,
75
+ packageVersion: string | undefined,
76
+ packageName: string,
77
+ ): TailwindMajorVersion {
78
+ if (configuredMajor !== undefined) {
79
+ validateInstalledVersion(packageVersion, configuredMajor, packageName)
80
+ return configuredMajor
81
+ }
82
+
83
+ const installedMajor = resolveInstalledMajorVersion(packageVersion)
84
+ if (installedMajor !== undefined) {
85
+ return installedMajor
86
+ }
87
+
88
+ throw new Error(
89
+ `Unable to infer Tailwind CSS major version from resolved package "${packageName}" (${packageVersion ?? 'unknown'}). Set "tailwindcss.version" to 2, 3, or 4 explicitly.`,
90
+ )
91
+ }
92
+
93
+ function createCollector(
94
+ packageInfo: PackageInfo,
95
+ options: NormalizedTailwindCssPatchOptions,
96
+ majorVersion: TailwindMajorVersion,
97
+ snapshotFactory: () => string,
98
+ ): TailwindCollector {
99
+ if (majorVersion === 4) {
100
+ return new TailwindV4Collector(packageInfo, options, snapshotFactory)
101
+ }
102
+
103
+ return new RuntimeCollector(packageInfo, options, majorVersion, snapshotFactory)
104
+ }
105
+
106
+ export class TailwindcssPatcher {
107
+ public readonly options: NormalizedTailwindCssPatchOptions
108
+ public readonly packageInfo: PackageInfo
109
+ public readonly majorVersion: TailwindMajorVersion
110
+
111
+ private readonly cacheContext: {
112
+ fingerprint: string
113
+ metadata: CacheContextMetadata
114
+ }
115
+
116
+ private readonly cacheStore: CacheStore
117
+ private readonly collector: TailwindCollector
118
+ private patchMemo: PatchMemo | undefined
119
+
120
+ constructor(options: TailwindCssPatchOptions = {}) {
121
+ this.options = normalizeOptions(options)
122
+ const packageInfo = getPackageInfoSync(
123
+ this.options.tailwind.packageName,
124
+ this.options.tailwind.resolve,
125
+ )
126
+
127
+ if (!packageInfo) {
128
+ throw new Error(`Unable to locate Tailwind CSS package "${this.options.tailwind.packageName}".`)
129
+ }
130
+
131
+ this.packageInfo = packageInfo as PackageInfo
132
+ this.majorVersion = resolveMajorVersionOrThrow(
133
+ this.options.tailwind.versionHint,
134
+ this.packageInfo.version,
135
+ this.options.tailwind.packageName,
136
+ )
137
+
138
+ this.cacheContext = createCacheContextDescriptor(
139
+ this.options,
140
+ this.packageInfo,
141
+ this.majorVersion,
142
+ )
143
+ this.cacheStore = new CacheStore(this.options.cache, this.cacheContext)
144
+ this.collector = createCollector(
145
+ this.packageInfo,
146
+ this.options,
147
+ this.majorVersion,
148
+ () => this.createPatchSnapshot(),
149
+ )
150
+ }
151
+
152
+ async patch() {
153
+ const snapshot = this.collector.getPatchSnapshot()
154
+ if (this.patchMemo && this.patchMemo.snapshot === snapshot) {
155
+ return this.patchMemo.result
156
+ }
157
+
158
+ const result = await this.collector.patch()
159
+ this.patchMemo = {
160
+ result,
161
+ snapshot: this.collector.getPatchSnapshot(),
162
+ }
163
+ return result
164
+ }
165
+
166
+ async getPatchStatus() {
167
+ return this.collector.getPatchStatus()
168
+ }
169
+
170
+ getContexts() {
171
+ return this.collector.getContexts()
172
+ }
173
+
174
+ private createPatchSnapshot() {
175
+ const entries: string[] = []
176
+ const pushSnapshot = (filePath: string) => {
177
+ if (!fs.pathExistsSync(filePath)) {
178
+ entries.push(`${filePath}:missing`)
179
+ return
180
+ }
181
+
182
+ const stat = fs.statSync(filePath)
183
+ entries.push(`${filePath}:${stat.size}:${Math.trunc(stat.mtimeMs)}`)
184
+ }
185
+
186
+ if (this.options.features.exposeContext.enabled && (this.majorVersion === 2 || this.majorVersion === 3)) {
187
+ if (this.majorVersion === 2) {
188
+ pushSnapshot(path.resolve(this.packageInfo.rootPath, 'lib/jit/processTailwindFeatures.js'))
189
+ pushSnapshot(path.resolve(this.packageInfo.rootPath, 'lib/jit/index.js'))
190
+ }
191
+ else {
192
+ pushSnapshot(path.resolve(this.packageInfo.rootPath, 'lib/processTailwindFeatures.js'))
193
+ const pluginPath = ['lib/plugin.js', 'lib/index.js']
194
+ .map(file => path.resolve(this.packageInfo.rootPath, file))
195
+ .find(file => fs.pathExistsSync(file))
196
+ if (pluginPath) {
197
+ pushSnapshot(pluginPath)
198
+ }
199
+ }
200
+ }
201
+
202
+ if (this.options.features.extendLengthUnits?.enabled) {
203
+ if (this.majorVersion === 3) {
204
+ const target = this.options.features.extendLengthUnits.lengthUnitsFilePath ?? 'lib/util/dataTypes.js'
205
+ pushSnapshot(path.resolve(this.packageInfo.rootPath, target))
206
+ }
207
+ else if (this.majorVersion === 4) {
208
+ const distDir = path.resolve(this.packageInfo.rootPath, 'dist')
209
+ if (fs.pathExistsSync(distDir)) {
210
+ const chunkNames = fs.readdirSync(distDir)
211
+ .filter(entry => entry.endsWith('.js') || entry.endsWith('.mjs'))
212
+ .sort()
213
+ for (const chunkName of chunkNames) {
214
+ pushSnapshot(path.join(distDir, chunkName))
215
+ }
216
+ }
217
+ else {
218
+ entries.push(`${distDir}:missing`)
219
+ }
220
+ }
221
+ }
222
+
223
+ return entries.join('|')
224
+ }
225
+
226
+ private async collectClassSet(): Promise<Set<string>> {
227
+ if (this.majorVersion === 4) {
228
+ return this.collector.collectClassSet()
229
+ }
230
+
231
+ const contexts = this.getContexts()
232
+ return collectClassesFromContexts(contexts, this.options.filter)
233
+ }
234
+
235
+ private async runTailwindBuildIfNeeded() {
236
+ await this.collector.runTailwindBuildIfNeeded?.()
237
+ }
238
+
239
+ private debugCacheRead(meta: CacheReadMeta) {
240
+ if (meta.hit) {
241
+ logger.debug(
242
+ `[cache] hit fingerprint=${meta.fingerprint?.slice(0, 12) ?? 'n/a'} schema=${meta.schemaVersion ?? 'legacy'} ${meta.details.join('; ')}`,
243
+ )
244
+ return
245
+ }
246
+
247
+ logger.debug(
248
+ `[cache] miss reason=${meta.reason} fingerprint=${meta.fingerprint?.slice(0, 12) ?? 'n/a'} schema=${meta.schemaVersion ?? 'legacy'} ${meta.details.join('; ')}`,
249
+ )
250
+ }
251
+
252
+ private async mergeWithCache(set: Set<string>) {
253
+ if (!this.options.cache.enabled) {
254
+ return set
255
+ }
256
+
257
+ const { data: existing, meta } = await this.cacheStore.readWithMeta()
258
+ this.debugCacheRead(meta)
259
+ if (this.options.cache.strategy === 'merge') {
260
+ for (const value of existing) {
261
+ set.add(value)
262
+ }
263
+ const writeTarget = this.areSetsEqual(existing, set)
264
+ ? undefined
265
+ : await this.cacheStore.write(set)
266
+ if (writeTarget) {
267
+ logger.debug(`[cache] stored ${set.size} classes -> ${writeTarget}`)
268
+ }
269
+ }
270
+ else {
271
+ if (set.size > 0) {
272
+ const writeTarget = this.areSetsEqual(existing, set)
273
+ ? undefined
274
+ : await this.cacheStore.write(set)
275
+ if (writeTarget) {
276
+ logger.debug(`[cache] stored ${set.size} classes -> ${writeTarget}`)
277
+ }
278
+ }
279
+ else {
280
+ return existing
281
+ }
282
+ }
283
+
284
+ return set
285
+ }
286
+
287
+ private mergeWithCacheSync(set: Set<string>) {
288
+ if (!this.options.cache.enabled) {
289
+ return set
290
+ }
291
+
292
+ const { data: existing, meta } = this.cacheStore.readWithMetaSync()
293
+ this.debugCacheRead(meta)
294
+ if (this.options.cache.strategy === 'merge') {
295
+ for (const value of existing) {
296
+ set.add(value)
297
+ }
298
+ const writeTarget = this.areSetsEqual(existing, set)
299
+ ? undefined
300
+ : this.cacheStore.writeSync(set)
301
+ if (writeTarget) {
302
+ logger.debug(`[cache] stored ${set.size} classes -> ${writeTarget}`)
303
+ }
304
+ }
305
+ else {
306
+ if (set.size > 0) {
307
+ const writeTarget = this.areSetsEqual(existing, set)
308
+ ? undefined
309
+ : this.cacheStore.writeSync(set)
310
+ if (writeTarget) {
311
+ logger.debug(`[cache] stored ${set.size} classes -> ${writeTarget}`)
312
+ }
313
+ }
314
+ else {
315
+ return existing
316
+ }
317
+ }
318
+
319
+ return set
320
+ }
321
+
322
+ private areSetsEqual(a: Set<string>, b: Set<string>) {
323
+ if (a.size !== b.size) {
324
+ return false
325
+ }
326
+
327
+ for (const value of a) {
328
+ if (!b.has(value)) {
329
+ return false
330
+ }
331
+ }
332
+
333
+ return true
334
+ }
335
+
336
+ async getClassSet() {
337
+ await this.runTailwindBuildIfNeeded()
338
+ const set = await this.collectClassSet()
339
+ return this.mergeWithCache(set)
340
+ }
341
+
342
+ getClassSetSync(): Set<string> | undefined {
343
+ if (this.majorVersion === 4) {
344
+ throw new Error('getClassSetSync is not supported for Tailwind CSS v4 projects. Use getClassSet instead.')
345
+ }
346
+
347
+ const contexts = this.getContexts()
348
+ const set = collectClassesFromContexts(contexts, this.options.filter)
349
+ const merged = this.mergeWithCacheSync(set)
350
+ if (contexts.length === 0 && merged.size === 0) {
351
+ return undefined
352
+ }
353
+ return merged
354
+ }
355
+
356
+ async extract(options?: { write?: boolean }): Promise<ExtractResult> {
357
+ const shouldWrite = options?.write ?? this.options.output.enabled
358
+ const classSet = await this.getClassSet()
359
+ const classList = Array.from(classSet)
360
+
361
+ const result: ExtractResult = {
362
+ classList,
363
+ classSet,
364
+ }
365
+
366
+ if (!shouldWrite || !this.options.output.file) {
367
+ return result
368
+ }
369
+
370
+ const target = path.resolve(this.options.output.file)
371
+ await fs.ensureDir(path.dirname(target))
372
+
373
+ if (this.options.output.format === 'json') {
374
+ const spaces = typeof this.options.output.pretty === 'number' ? this.options.output.pretty : undefined
375
+ await fs.writeJSON(target, classList, { spaces })
376
+ }
377
+ else {
378
+ await fs.writeFile(target, `${classList.join('\n')}\n`, 'utf8')
379
+ }
380
+
381
+ logger.success(`Tailwind CSS class list saved to ${target.replace(process.cwd(), '.')}`)
382
+
383
+ return {
384
+ ...result,
385
+ filename: target,
386
+ }
387
+ }
388
+
389
+ async clearCache(options?: CacheClearOptions): Promise<CacheClearResult> {
390
+ const result = await this.cacheStore.clear(options)
391
+ logger.debug(
392
+ `[cache] clear scope=${result.scope} contexts=${result.contextsRemoved} entries=${result.entriesRemoved} files=${result.filesRemoved}`,
393
+ )
394
+ return result
395
+ }
396
+
397
+ // Backwards compatibility helper used by tests and API consumers.
398
+ extractValidCandidates = extractCandidates
399
+
400
+ async collectContentTokens(options?: { cwd?: string, sources?: SourceEntry[] }): Promise<TailwindTokenReport> {
401
+ return extractProjectCandidatesWithPositions({
402
+ cwd: options?.cwd ?? this.options.projectRoot,
403
+ sources: options?.sources ?? this.options.tailwind.v4?.sources ?? [],
404
+ })
405
+ }
406
+
407
+ async collectContentTokensByFile(options?: {
408
+ cwd?: string
409
+ sources?: SourceEntry[]
410
+ key?: TailwindTokenFileKey
411
+ stripAbsolutePaths?: boolean
412
+ }): Promise<TailwindTokenByFileMap> {
413
+ const collectContentOptions = {
414
+ ...(options?.cwd === undefined ? {} : { cwd: options.cwd }),
415
+ ...(options?.sources === undefined ? {} : { sources: options.sources }),
416
+ }
417
+ const report = await this.collectContentTokens(collectContentOptions)
418
+ const groupOptions = {
419
+ ...(options?.key === undefined ? {} : { key: options.key }),
420
+ ...(options?.stripAbsolutePaths === undefined ? {} : { stripAbsolutePaths: options.stripAbsolutePaths }),
421
+ }
422
+ return groupTokensByFile(report, groupOptions)
423
+ }
424
+ }
@@ -0,0 +1,12 @@
1
+ import _babelGenerate from '@babel/generator'
2
+ import _babelTraverse from '@babel/traverse'
3
+
4
+ export { parse, parseExpression } from '@babel/parser'
5
+
6
+ export function _interopDefaultCompat(e: any) {
7
+ return e && typeof e === 'object' && 'default' in e ? e.default : e
8
+ }
9
+
10
+ export const generate = _interopDefaultCompat(_babelGenerate) as typeof _babelGenerate
11
+
12
+ export const traverse = _interopDefaultCompat(_babelTraverse) as typeof _babelTraverse
@@ -0,0 +1,212 @@
1
+ import type { PackageInfo } from 'local-pkg'
2
+ import type { NormalizedTailwindCssPatchOptions } from '../options/types'
3
+ import type { CacheContextDescriptor, CacheContextMetadata } from './types'
4
+ import { createHash } from 'node:crypto'
5
+ import process from 'node:process'
6
+ import fs from 'fs-extra'
7
+ import path from 'pathe'
8
+ import { pkgVersion } from '../constants'
9
+ import { CACHE_FINGERPRINT_VERSION } from './types'
10
+
11
+ const DEFAULT_TAILWIND_CONFIG_FILES = [
12
+ 'tailwind.config.js',
13
+ 'tailwind.config.cjs',
14
+ 'tailwind.config.mjs',
15
+ 'tailwind.config.ts',
16
+ 'tailwind.config.cts',
17
+ 'tailwind.config.mts',
18
+ ]
19
+
20
+ function normalizePathname(value: string) {
21
+ return path.normalize(value).replaceAll('\\', '/')
22
+ }
23
+
24
+ function resolveRealpathSyncSafe(value: string): string {
25
+ const resolved = path.resolve(value)
26
+ try {
27
+ return normalizePathname(fs.realpathSync(resolved))
28
+ }
29
+ catch {
30
+ return normalizePathname(resolved)
31
+ }
32
+ }
33
+
34
+ function resolveFileMtimeMsSync(value: string | undefined): number | undefined {
35
+ if (!value) {
36
+ return undefined
37
+ }
38
+
39
+ try {
40
+ const stat = fs.statSync(value)
41
+ if (!stat.isFile()) {
42
+ return undefined
43
+ }
44
+ return stat.mtimeMs
45
+ }
46
+ catch {
47
+ return undefined
48
+ }
49
+ }
50
+
51
+ function resolveTailwindConfigPath(
52
+ options: NormalizedTailwindCssPatchOptions,
53
+ majorVersion: 2 | 3 | 4,
54
+ ): string | undefined {
55
+ const tailwind = options.tailwind
56
+ const baseDir = tailwind.cwd ?? options.projectRoot
57
+
58
+ const configured = (() => {
59
+ if (majorVersion === 2 && tailwind.v2?.config) {
60
+ return tailwind.v2.config
61
+ }
62
+ if (majorVersion === 3 && tailwind.v3?.config) {
63
+ return tailwind.v3.config
64
+ }
65
+ return tailwind.config
66
+ })()
67
+
68
+ if (configured) {
69
+ const absolute = path.isAbsolute(configured) ? configured : path.resolve(baseDir, configured)
70
+ if (fs.pathExistsSync(absolute)) {
71
+ return resolveRealpathSyncSafe(absolute)
72
+ }
73
+ }
74
+
75
+ for (const candidate of DEFAULT_TAILWIND_CONFIG_FILES) {
76
+ const absolute = path.resolve(baseDir, candidate)
77
+ if (fs.pathExistsSync(absolute)) {
78
+ return resolveRealpathSyncSafe(absolute)
79
+ }
80
+ }
81
+
82
+ return undefined
83
+ }
84
+
85
+ function stableSerialize(input: unknown): string {
86
+ if (input === null) {
87
+ return 'null'
88
+ }
89
+
90
+ if (typeof input === 'string') {
91
+ return JSON.stringify(input)
92
+ }
93
+
94
+ if (typeof input === 'number' || typeof input === 'boolean') {
95
+ return JSON.stringify(input)
96
+ }
97
+
98
+ if (Array.isArray(input)) {
99
+ return `[${input.map(item => stableSerialize(item)).join(',')}]`
100
+ }
101
+
102
+ if (typeof input === 'object') {
103
+ const entries = Object.entries(input as Record<string, unknown>)
104
+ .filter(([, value]) => value !== undefined)
105
+ .sort(([a], [b]) => a.localeCompare(b))
106
+ .map(([key, value]) => `${JSON.stringify(key)}:${stableSerialize(value)}`)
107
+ return `{${entries.join(',')}}`
108
+ }
109
+
110
+ return JSON.stringify(String(input))
111
+ }
112
+
113
+ function hash(input: string): string {
114
+ return createHash('sha256').update(input).digest('hex')
115
+ }
116
+
117
+ function toFingerprintOptions(normalized: NormalizedTailwindCssPatchOptions) {
118
+ return {
119
+ overwrite: normalized.overwrite,
120
+ output: {
121
+ removeUniversalSelector: normalized.output.removeUniversalSelector,
122
+ format: normalized.output.format,
123
+ },
124
+ features: normalized.features,
125
+ tailwind: {
126
+ packageName: normalized.tailwind.packageName,
127
+ cwd: normalized.tailwind.cwd,
128
+ config: normalized.tailwind.config,
129
+ postcssPlugin: normalized.tailwind.postcssPlugin,
130
+ versionHint: normalized.tailwind.versionHint,
131
+ v2: normalized.tailwind.v2,
132
+ v3: normalized.tailwind.v3,
133
+ v4: normalized.tailwind.v4,
134
+ },
135
+ }
136
+ }
137
+
138
+ export function createCacheContextDescriptor(
139
+ options: NormalizedTailwindCssPatchOptions,
140
+ packageInfo: PackageInfo,
141
+ majorVersion: 2 | 3 | 4,
142
+ ): CacheContextDescriptor {
143
+ const projectRootRealpath = resolveRealpathSyncSafe(options.projectRoot)
144
+ const processCwdRealpath = resolveRealpathSyncSafe(process.cwd())
145
+ const cacheCwdRealpath = resolveRealpathSyncSafe(options.cache.cwd)
146
+ const tailwindPackageRootRealpath = resolveRealpathSyncSafe(packageInfo.rootPath)
147
+ const tailwindConfigPath = resolveTailwindConfigPath(options, majorVersion)
148
+ const tailwindConfigMtimeMs = resolveFileMtimeMsSync(tailwindConfigPath)
149
+
150
+ const optionsHash = hash(stableSerialize(toFingerprintOptions(options)))
151
+
152
+ const metadata: CacheContextMetadata = {
153
+ fingerprintVersion: CACHE_FINGERPRINT_VERSION,
154
+ projectRootRealpath,
155
+ processCwdRealpath,
156
+ cacheCwdRealpath,
157
+ ...(tailwindConfigPath === undefined ? {} : { tailwindConfigPath }),
158
+ ...(tailwindConfigMtimeMs === undefined ? {} : { tailwindConfigMtimeMs }),
159
+ tailwindPackageRootRealpath,
160
+ tailwindPackageVersion: packageInfo.version ?? 'unknown',
161
+ patcherVersion: pkgVersion,
162
+ majorVersion,
163
+ optionsHash,
164
+ }
165
+
166
+ const fingerprint = hash(stableSerialize(metadata))
167
+
168
+ return {
169
+ fingerprint,
170
+ metadata,
171
+ }
172
+ }
173
+
174
+ export function explainContextMismatch(
175
+ current: CacheContextMetadata,
176
+ cached: CacheContextMetadata,
177
+ ): string[] {
178
+ const reasons: string[] = []
179
+
180
+ if (current.projectRootRealpath !== cached.projectRootRealpath) {
181
+ reasons.push(`project-root changed: ${cached.projectRootRealpath} -> ${current.projectRootRealpath}`)
182
+ }
183
+ if (current.processCwdRealpath !== cached.processCwdRealpath) {
184
+ reasons.push(`process-cwd changed: ${cached.processCwdRealpath} -> ${current.processCwdRealpath}`)
185
+ }
186
+ if (current.cacheCwdRealpath !== cached.cacheCwdRealpath) {
187
+ reasons.push(`cache-cwd changed: ${cached.cacheCwdRealpath} -> ${current.cacheCwdRealpath}`)
188
+ }
189
+ if ((current.tailwindConfigPath ?? '') !== (cached.tailwindConfigPath ?? '')) {
190
+ reasons.push(`tailwind-config path changed: ${cached.tailwindConfigPath ?? '<none>'} -> ${current.tailwindConfigPath ?? '<none>'}`)
191
+ }
192
+ if ((current.tailwindConfigMtimeMs ?? -1) !== (cached.tailwindConfigMtimeMs ?? -1)) {
193
+ reasons.push('tailwind-config mtime changed')
194
+ }
195
+ if (current.tailwindPackageRootRealpath !== cached.tailwindPackageRootRealpath) {
196
+ reasons.push(`tailwind-package root changed: ${cached.tailwindPackageRootRealpath} -> ${current.tailwindPackageRootRealpath}`)
197
+ }
198
+ if (current.tailwindPackageVersion !== cached.tailwindPackageVersion) {
199
+ reasons.push(`tailwind-package version changed: ${cached.tailwindPackageVersion} -> ${current.tailwindPackageVersion}`)
200
+ }
201
+ if (current.patcherVersion !== cached.patcherVersion) {
202
+ reasons.push(`patcher version changed: ${cached.patcherVersion} -> ${current.patcherVersion}`)
203
+ }
204
+ if (current.majorVersion !== cached.majorVersion) {
205
+ reasons.push(`major version changed: ${cached.majorVersion} -> ${current.majorVersion}`)
206
+ }
207
+ if (current.optionsHash !== cached.optionsHash) {
208
+ reasons.push(`patch options hash changed: ${cached.optionsHash.slice(0, 12)} -> ${current.optionsHash.slice(0, 12)}`)
209
+ }
210
+
211
+ return reasons
212
+ }