Skip to content

Commit 91f408c

Browse files
committed
chore: bump version
1 parent 844e641 commit 91f408c

File tree

5 files changed

+186
-7
lines changed

5 files changed

+186
-7
lines changed

.changeset/add-sync-class-set.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'tailwindcss-patch': minor
3+
---
4+
5+
Add `getClassSetSync` along with synchronous cache utilities so consumers that cannot await the async API can still collect Tailwind classes.

packages/tailwindcss-patch/src/api/tailwindcss-patcher.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,15 @@ export class TailwindcssPatcher {
139139
return collectClassesFromContexts(contexts, this.options.filter)
140140
}
141141

142+
private collectClassSetSync(): Set<string> {
143+
if (this.majorVersion === 4) {
144+
throw new Error('getClassSetSync is not supported for Tailwind CSS v4 projects. Use getClassSet instead.')
145+
}
146+
147+
const contexts = this.getContexts()
148+
return collectClassesFromContexts(contexts, this.options.filter)
149+
}
150+
142151
private async mergeWithCache(set: Set<string>) {
143152
if (!this.options.cache.enabled) {
144153
return set
@@ -163,12 +172,41 @@ export class TailwindcssPatcher {
163172
return set
164173
}
165174

175+
private mergeWithCacheSync(set: Set<string>) {
176+
if (!this.options.cache.enabled) {
177+
return set
178+
}
179+
180+
const existing = this.cacheStore.readSync()
181+
if (this.options.cache.strategy === 'merge') {
182+
for (const value of existing) {
183+
set.add(value)
184+
}
185+
this.cacheStore.writeSync(set)
186+
}
187+
else {
188+
if (set.size > 0) {
189+
this.cacheStore.writeSync(set)
190+
}
191+
else {
192+
return existing
193+
}
194+
}
195+
196+
return set
197+
}
198+
166199
async getClassSet() {
167200
await this.runTailwindBuildIfNeeded()
168201
const set = await this.collectClassSet()
169202
return this.mergeWithCache(set)
170203
}
171204

205+
getClassSetSync() {
206+
const set = this.collectClassSetSync()
207+
return this.mergeWithCacheSync(set)
208+
}
209+
172210
async extract(options?: { write?: boolean }): Promise<ExtractResult> {
173211
const shouldWrite = options?.write ?? this.options.output.enabled
174212
const classSet = await this.getClassSet()

packages/tailwindcss-patch/src/cache/store.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ export class CacheStore {
99
await fs.ensureDir(this.options.dir)
1010
}
1111

12+
private ensureDirSync() {
13+
fs.ensureDirSync(this.options.dir)
14+
}
15+
1216
async write(data: Set<string>): Promise<string | undefined> {
1317
if (!this.options.enabled) {
1418
return undefined
@@ -25,6 +29,22 @@ export class CacheStore {
2529
}
2630
}
2731

32+
writeSync(data: Set<string>): string | undefined {
33+
if (!this.options.enabled) {
34+
return undefined
35+
}
36+
37+
try {
38+
this.ensureDirSync()
39+
fs.writeJSONSync(this.options.path, Array.from(data))
40+
return this.options.path
41+
}
42+
catch (error) {
43+
logger.error('Unable to persist Tailwind class cache', error)
44+
return undefined
45+
}
46+
}
47+
2848
async read(): Promise<Set<string>> {
2949
if (!this.options.enabled) {
3050
return new Set()
@@ -53,4 +73,33 @@ export class CacheStore {
5373

5474
return new Set()
5575
}
76+
77+
readSync(): Set<string> {
78+
if (!this.options.enabled) {
79+
return new Set()
80+
}
81+
82+
try {
83+
const exists = fs.pathExistsSync(this.options.path)
84+
if (!exists) {
85+
return new Set()
86+
}
87+
88+
const data = fs.readJSONSync(this.options.path)
89+
if (Array.isArray(data)) {
90+
return new Set(data.filter((item): item is string => typeof item === 'string'))
91+
}
92+
}
93+
catch (error) {
94+
logger.warn('Unable to read Tailwind class cache, removing invalid file.', error)
95+
try {
96+
fs.removeSync(this.options.path)
97+
}
98+
catch (cleanupError) {
99+
logger.error('Failed to clean up invalid cache file', cleanupError)
100+
}
101+
}
102+
103+
return new Set()
104+
}
56105
}

packages/tailwindcss-patch/test/api.tailwindcss-patcher.test.ts

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
import os from 'node:os'
22
import fs from 'fs-extra'
33
import path from 'pathe'
4-
import { afterEach, describe, expect, it } from 'vitest'
4+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
55
import { TailwindcssPatcher } from '@/api/tailwindcss-patcher'
66

77
const fixturesRoot = path.resolve(__dirname, 'fixtures/v4')
8-
let tempDir: string | undefined
8+
let tempDir: string
9+
10+
beforeEach(async () => {
11+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'tw-patch-'))
12+
})
913

1014
afterEach(async () => {
11-
if (tempDir) {
12-
await fs.remove(tempDir)
13-
tempDir = undefined
14-
}
15+
await fs.remove(tempDir)
16+
vi.restoreAllMocks()
1517
})
1618

1719
describe('TailwindcssPatcher', () => {
1820
it('collects classes for Tailwind CSS v4 projects', async () => {
19-
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'tw-patch-'))
2021
const outputFile = path.join(tempDir, 'classes.json')
2122

2223
const patcher = new TailwindcssPatcher({
@@ -39,4 +40,68 @@ describe('TailwindcssPatcher', () => {
3940
expect(result.classList.length).toBeGreaterThan(0)
4041
expect(await fs.pathExists(outputFile)).toBe(true)
4142
})
43+
44+
it('collects classes synchronously from runtime contexts', () => {
45+
const cacheFile = path.join(tempDir, 'cache.json')
46+
const patcher = new TailwindcssPatcher({
47+
overwrite: false,
48+
cache: {
49+
enabled: true,
50+
dir: tempDir,
51+
file: 'cache.json',
52+
},
53+
tailwind: {
54+
packageName: 'tailwindcss-3',
55+
version: 3,
56+
},
57+
})
58+
59+
const classCache = new Map<string, any>([
60+
['foo', []],
61+
['bar', []],
62+
])
63+
vi.spyOn(patcher, 'getContexts').mockReturnValue([
64+
{
65+
classCache,
66+
} as any,
67+
])
68+
69+
const result = patcher.getClassSetSync()
70+
71+
expect(result.has('foo')).toBe(true)
72+
expect(result.has('bar')).toBe(true)
73+
expect(fs.pathExistsSync(cacheFile)).toBe(true)
74+
const cacheContent = fs.readJSONSync(cacheFile)
75+
expect(cacheContent).toEqual(expect.arrayContaining(['foo', 'bar']))
76+
})
77+
78+
it('falls back to cached classes when runtime contexts are empty', () => {
79+
const cacheFile = path.join(tempDir, 'cache.json')
80+
fs.writeJSONSync(cacheFile, ['cached-class'])
81+
82+
const patcher = new TailwindcssPatcher({
83+
overwrite: false,
84+
cache: {
85+
enabled: true,
86+
dir: tempDir,
87+
file: 'cache.json',
88+
strategy: 'overwrite',
89+
},
90+
tailwind: {
91+
packageName: 'tailwindcss-3',
92+
version: 3,
93+
},
94+
})
95+
96+
vi.spyOn(patcher, 'getContexts').mockReturnValue([
97+
{
98+
classCache: new Map<string, any>(),
99+
} as any,
100+
])
101+
102+
const result = patcher.getClassSetSync()
103+
104+
expect(result.size).toBe(1)
105+
expect(result.has('cached-class')).toBe(true)
106+
})
42107
})

packages/tailwindcss-patch/test/cache.store.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,26 @@ describe('CacheStore', () => {
5252
expect(restored.size).toBe(0)
5353
expect(await fs.pathExists(cachePath)).toBe(false)
5454
})
55+
56+
it('reads and writes cache data synchronously', () => {
57+
const cachePath = path.join(tempDir, 'cache.json')
58+
const store = new CacheStore({
59+
enabled: true,
60+
cwd: tempDir,
61+
dir: tempDir,
62+
file: 'cache.json',
63+
path: cachePath,
64+
strategy: 'merge',
65+
})
66+
67+
const initial = store.readSync()
68+
expect(initial.size).toBe(0)
69+
70+
const data = new Set(['foo'])
71+
store.writeSync(data)
72+
73+
const restored = store.readSync()
74+
expect(restored.size).toBe(1)
75+
expect(restored.has('foo')).toBe(true)
76+
})
5577
})

0 commit comments

Comments
 (0)