Skip to content

Commit fa6e410

Browse files
DunqingBoshen
andauthored
Improve config resolution caching with directory-based cache (#432)
Co-authored-by: Boshen <boshenc@gmail.com>
1 parent fac7c36 commit fa6e410

1 file changed

Lines changed: 104 additions & 24 deletions

File tree

src/config.ts

Lines changed: 104 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,41 @@ import type { UnifiedApi } from './types'
1111
import { loadV3 } from './versions/v3'
1212
import { loadV4 } from './versions/v4'
1313

14+
/**
15+
* Cache a value for all directories from `inputDir` up to `targetDir` (inclusive).
16+
* Stops early if an existing cache entry is found.
17+
*
18+
* How it works:
19+
*
20+
* For a file at '/repo/packages/ui/src/Button.tsx' with config at '/repo/package.json'
21+
*
22+
* `cacheForDirs(cache, '/repo/packages/ui/src', '/repo/package.json', '/repo')`
23+
*
24+
* Caches:
25+
* - '/repo/packages/ui/src' -> '/repo/package.json'
26+
* - '/repo/packages/ui' -> '/repo/package.json'
27+
* - '/repo/packages' -> '/repo/package.json'
28+
* - '/repo' -> '/repo/package.json'
29+
*/
30+
function cacheForDirs<V>(
31+
cache: { set(key: string, value: V): void; get(key: string): V | undefined },
32+
inputDir: string,
33+
value: V,
34+
targetDir: string,
35+
makeKey: (dir: string) => string = (dir) => dir,
36+
): void {
37+
let dir = inputDir
38+
while (dir !== path.dirname(dir) && dir.length >= targetDir.length) {
39+
const key = makeKey(dir)
40+
// Stop caching if we hit an existing entry
41+
if (cache.get(key) !== undefined) break
42+
43+
cache.set(key, value)
44+
if (dir === targetDir) break
45+
dir = path.dirname(dir)
46+
}
47+
}
48+
1449
let pathToApiMap = expiringMap<string | null, Promise<UnifiedApi>>(10_000)
1550

1651
export async function getTailwindConfig(options: ParserOptions): Promise<any> {
@@ -34,7 +69,7 @@ export async function getTailwindConfig(options: ParserOptions): Promise<any> {
3469
//
3570
// These lookups can take a bit so we cache them. This is especially important
3671
// for files with lots of embedded languages (e.g. Vue bindings).
37-
let [configDir, configPath] = await resolvePrettierConfigPath(options.filepath)
72+
let [configDir, configPath] = await resolvePrettierConfigPath(options.filepath, inputDir)
3873

3974
// Locate Tailwind CSS itself
4075
//
@@ -120,31 +155,56 @@ export async function getTailwindConfig(options: ParserOptions): Promise<any> {
120155
return pathToApiMap.remember(`${pkgDir}:${stylesheet}`, () => loadV4(mod, stylesheet))
121156
}
122157

123-
let prettierConfigCache = expiringMap<string, Promise<string | null>>(10_000)
158+
let prettierConfigCache = expiringMap<string, string | null>(10_000)
124159

125-
async function resolvePrettierConfigPath(filePath: string): Promise<[string, string | null]> {
126-
let prettierConfig = await prettierConfigCache.remember(filePath, async () => {
160+
async function resolvePrettierConfigPath(
161+
filePath: string,
162+
inputDir: string,
163+
): Promise<[string, string | null]> {
164+
// Check cache for this directory
165+
let cached = prettierConfigCache.get(inputDir)
166+
if (cached !== undefined) {
167+
return cached ? [path.dirname(cached), cached] : [process.cwd(), null]
168+
}
169+
170+
const resolve = async () => {
127171
try {
128172
return await prettier.resolveConfigFile(filePath)
129173
} catch (err) {
130174
console.error('prettier-config-not-found', 'Failed to resolve Prettier Config')
131175
console.error('prettier-config-not-found-err', err)
132176
return null
133177
}
134-
})
178+
}
179+
180+
let prettierConfig = await resolve()
181+
182+
// Cache all directories from inputDir up to config location
183+
if (prettierConfig) {
184+
cacheForDirs(prettierConfigCache, inputDir, prettierConfig, path.dirname(prettierConfig))
185+
} else {
186+
prettierConfigCache.set(inputDir, null)
187+
}
135188

136189
return prettierConfig ? [path.dirname(prettierConfig), prettierConfig] : [process.cwd(), null]
137190
}
138191

139-
let resolvedModCache = expiringMap<string, Promise<[any | null, string | null]>>(10_000)
192+
let resolvedModCache = expiringMap<string, [any | null, string | null]>(10_000)
140193

141194
async function resolveTailwindPath(
142195
options: ParserOptions,
143196
baseDir: string,
144197
): Promise<[any | null, string | null]> {
145198
let pkgName = options.tailwindPackageName ?? 'tailwindcss'
199+
let makeKey = (dir: string) => `${pkgName}:${dir}`
200+
201+
// Check cache for this directory
202+
let cached = resolvedModCache.get(makeKey(baseDir))
203+
if (cached !== undefined) {
204+
return cached
205+
}
146206

147-
return await resolvedModCache.remember(`${pkgName}:${baseDir}`, async () => {
207+
let resolve = async () => {
148208
let pkgDir: string | null = null
149209
let mod: any | null = null
150210

@@ -156,8 +216,20 @@ async function resolveTailwindPath(
156216
pkgDir = path.dirname(pkgFile)
157217
} catch {}
158218

159-
return [mod, pkgDir] as const
160-
})
219+
return [mod, pkgDir] as [any | null, string | null]
220+
}
221+
222+
let result = await resolve()
223+
224+
// Cache all directories from baseDir up to package location
225+
let [, pkgDir] = result
226+
if (pkgDir) {
227+
cacheForDirs(resolvedModCache, baseDir, result, pkgDir, makeKey)
228+
} else {
229+
resolvedModCache.set(makeKey(baseDir), result)
230+
}
231+
232+
return result
161233
}
162234

163235
function resolveJsConfigPath(options: ParserOptions, configDir: string): string | null {
@@ -168,23 +240,31 @@ function resolveJsConfigPath(options: ParserOptions, configDir: string): string
168240
}
169241

170242
let configPathCache = new Map<string, string | null>()
171-
function findClosestJsConfig(inputDir: string): string | null {
172-
let configPath: string | null | undefined = configPathCache.get(inputDir)
173243

174-
if (configPath === undefined) {
175-
try {
176-
let foundPath = escalade(inputDir, (_, names) => {
177-
if (names.includes('tailwind.config.js')) return 'tailwind.config.js'
178-
if (names.includes('tailwind.config.cjs')) return 'tailwind.config.cjs'
179-
if (names.includes('tailwind.config.mjs')) return 'tailwind.config.mjs'
180-
if (names.includes('tailwind.config.ts')) return 'tailwind.config.ts'
181-
})
182-
183-
configPath = foundPath ?? null
184-
} catch {}
244+
function findClosestJsConfig(inputDir: string): string | null {
245+
// Check cache for this directory
246+
let cached = configPathCache.get(inputDir)
247+
if (cached !== undefined) {
248+
return cached
249+
}
185250

186-
configPath ??= null
187-
configPathCache.set(inputDir, configPath)
251+
// Resolve
252+
let configPath: string | null = null
253+
try {
254+
let foundPath = escalade(inputDir, (_, names) => {
255+
if (names.includes('tailwind.config.js')) return 'tailwind.config.js'
256+
if (names.includes('tailwind.config.cjs')) return 'tailwind.config.cjs'
257+
if (names.includes('tailwind.config.mjs')) return 'tailwind.config.mjs'
258+
if (names.includes('tailwind.config.ts')) return 'tailwind.config.ts'
259+
})
260+
configPath = foundPath ?? null
261+
} catch {}
262+
263+
// Cache all directories from inputDir up to config location
264+
if (configPath) {
265+
cacheForDirs(configPathCache, inputDir, configPath, path.dirname(configPath))
266+
} else {
267+
configPathCache.set(inputDir, null)
188268
}
189269

190270
return configPath

0 commit comments

Comments
 (0)