Skip to content

Commit 237ca28

Browse files
committed
Improve config resolution caching with directory-based cache
1 parent c0f1a9e commit 237ca28

1 file changed

Lines changed: 102 additions & 24 deletions

File tree

src/config.ts

Lines changed: 102 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,28 +155,51 @@ 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)
159+
160+
async function resolvePrettierConfigPath(filePath: string, inputDir: string): Promise<[string, string | null]> {
161+
162+
// Check cache for this directory
163+
let cached = prettierConfigCache.get(inputDir)
164+
if (cached !== undefined) {
165+
return cached ? [path.dirname(cached), cached] : [process.cwd(), null]
166+
}
124167

125-
async function resolvePrettierConfigPath(filePath: string): Promise<[string, string | null]> {
126-
let prettierConfig = await prettierConfigCache.remember(filePath, async () => {
168+
const resolve = async () => {
127169
try {
128170
return await prettier.resolveConfigFile(filePath)
129171
} catch (err) {
130172
console.error('prettier-config-not-found', 'Failed to resolve Prettier Config')
131173
console.error('prettier-config-not-found-err', err)
132174
return null
133175
}
134-
})
176+
}
177+
178+
let prettierConfig = await resolve()
179+
180+
// Cache all directories from inputDir up to config location
181+
if (prettierConfig) {
182+
cacheForDirs(prettierConfigCache, inputDir, prettierConfig, path.dirname(prettierConfig))
183+
} else {
184+
prettierConfigCache.set(inputDir, null)
185+
}
135186

136187
return prettierConfig ? [path.dirname(prettierConfig), prettierConfig] : [process.cwd(), null]
137188
}
138189

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

141192
async function resolveTailwindPath(options: ParserOptions, baseDir: string): Promise<[any | null, string | null]> {
142193
let pkgName = options.tailwindPackageName ?? 'tailwindcss'
194+
let makeKey = (dir: string) => `${pkgName}:${dir}`
195+
196+
// Check cache for this directory
197+
let cached = resolvedModCache.get(makeKey(baseDir))
198+
if (cached !== undefined) {
199+
return cached
200+
}
143201

144-
return await resolvedModCache.remember(`${pkgName}:${baseDir}`, async () => {
202+
let resolve = async () => {
145203
let pkgDir: string | null = null
146204
let mod: any | null = null
147205

@@ -153,8 +211,20 @@ async function resolveTailwindPath(options: ParserOptions, baseDir: string): Pro
153211
pkgDir = path.dirname(pkgFile)
154212
} catch {}
155213

156-
return [mod, pkgDir] as const
157-
})
214+
return [mod, pkgDir] as [any | null, string | null]
215+
}
216+
217+
let result = await resolve()
218+
219+
// Cache all directories from baseDir up to package location
220+
let [, pkgDir] = result
221+
if (pkgDir) {
222+
cacheForDirs(resolvedModCache, baseDir, result, pkgDir, makeKey)
223+
} else {
224+
resolvedModCache.set(makeKey(baseDir), result)
225+
}
226+
227+
return result
158228
}
159229

160230
function resolveJsConfigPath(options: ParserOptions, configDir: string): string | null {
@@ -165,23 +235,31 @@ function resolveJsConfigPath(options: ParserOptions, configDir: string): string
165235
}
166236

167237
let configPathCache = new Map<string, string | null>()
168-
function findClosestJsConfig(inputDir: string): string | null {
169-
let configPath: string | null | undefined = configPathCache.get(inputDir)
170238

171-
if (configPath === undefined) {
172-
try {
173-
let foundPath = escalade(inputDir, (_, names) => {
174-
if (names.includes('tailwind.config.js')) return 'tailwind.config.js'
175-
if (names.includes('tailwind.config.cjs')) return 'tailwind.config.cjs'
176-
if (names.includes('tailwind.config.mjs')) return 'tailwind.config.mjs'
177-
if (names.includes('tailwind.config.ts')) return 'tailwind.config.ts'
178-
})
179-
180-
configPath = foundPath ?? null
181-
} catch {}
239+
function findClosestJsConfig(inputDir: string): string | null {
240+
// Check cache for this directory
241+
let cached = configPathCache.get(inputDir)
242+
if (cached !== undefined) {
243+
return cached
244+
}
182245

183-
configPath ??= null
184-
configPathCache.set(inputDir, configPath)
246+
// Resolve
247+
let configPath: string | null = null
248+
try {
249+
let foundPath = escalade(inputDir, (_, names) => {
250+
if (names.includes('tailwind.config.js')) return 'tailwind.config.js'
251+
if (names.includes('tailwind.config.cjs')) return 'tailwind.config.cjs'
252+
if (names.includes('tailwind.config.mjs')) return 'tailwind.config.mjs'
253+
if (names.includes('tailwind.config.ts')) return 'tailwind.config.ts'
254+
})
255+
configPath = foundPath ?? null
256+
} catch {}
257+
258+
// Cache all directories from inputDir up to config location
259+
if (configPath) {
260+
cacheForDirs(configPathCache, inputDir, configPath, path.dirname(configPath))
261+
} else {
262+
configPathCache.set(inputDir, null)
185263
}
186264

187265
return configPath

0 commit comments

Comments
 (0)