Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 102 additions & 24 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,41 @@ import type { UnifiedApi } from './types'
import { loadV3 } from './versions/v3'
import { loadV4 } from './versions/v4'

/**
* Cache a value for all directories from `inputDir` up to `targetDir` (inclusive).
* Stops early if an existing cache entry is found.
*
* How it works:
*
* For a file at '/repo/packages/ui/src/Button.tsx' with config at '/repo/package.json'
*
* `cacheForDirs(cache, '/repo/packages/ui/src', '/repo/package.json', '/repo')`
*
* Caches:
* - '/repo/packages/ui/src' -> '/repo/package.json'
* - '/repo/packages/ui' -> '/repo/package.json'
* - '/repo/packages' -> '/repo/package.json'
* - '/repo' -> '/repo/package.json'
*/
function cacheForDirs<V>(
cache: { set(key: string, value: V): void, get(key: string): V | undefined },
inputDir: string,
value: V,
targetDir: string,
makeKey: (dir: string) => string = (dir) => dir,
): void {
let dir = inputDir
while (dir !== path.dirname(dir) && dir.length >= targetDir.length) {
const key = makeKey(dir);
// Stop caching if we hit an existing entry
if (cache.get(key) !== undefined) break

cache.set(key, value)
if (dir === targetDir) break
dir = path.dirname(dir)
}
}

let pathToApiMap = expiringMap<string | null, Promise<UnifiedApi>>(10_000)

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

// Locate Tailwind CSS itself
//
Expand Down Expand Up @@ -120,28 +155,51 @@ export async function getTailwindConfig(options: ParserOptions): Promise<any> {
return pathToApiMap.remember(`${pkgDir}:${stylesheet}`, () => loadV4(mod, stylesheet))
}

let prettierConfigCache = expiringMap<string, Promise<string | null>>(10_000)
let prettierConfigCache = expiringMap<string, string | null>(10_000)

async function resolvePrettierConfigPath(filePath: string, inputDir: string): Promise<[string, string | null]> {

// Check cache for this directory
let cached = prettierConfigCache.get(inputDir)
if (cached !== undefined) {
return cached ? [path.dirname(cached), cached] : [process.cwd(), null]
}

async function resolvePrettierConfigPath(filePath: string): Promise<[string, string | null]> {
let prettierConfig = await prettierConfigCache.remember(filePath, async () => {
const resolve = async () => {
try {
return await prettier.resolveConfigFile(filePath)
} catch (err) {
console.error('prettier-config-not-found', 'Failed to resolve Prettier Config')
console.error('prettier-config-not-found-err', err)
return null
}
})
}

let prettierConfig = await resolve()

// Cache all directories from inputDir up to config location
if (prettierConfig) {
cacheForDirs(prettierConfigCache, inputDir, prettierConfig, path.dirname(prettierConfig))
} else {
prettierConfigCache.set(inputDir, null)
}

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

let resolvedModCache = expiringMap<string, Promise<[any | null, string | null]>>(10_000)
let resolvedModCache = expiringMap<string, [any | null, string | null]>(10_000)

async function resolveTailwindPath(options: ParserOptions, baseDir: string): Promise<[any | null, string | null]> {
let pkgName = options.tailwindPackageName ?? 'tailwindcss'
let makeKey = (dir: string) => `${pkgName}:${dir}`

// Check cache for this directory
let cached = resolvedModCache.get(makeKey(baseDir))
if (cached !== undefined) {
return cached
}

return await resolvedModCache.remember(`${pkgName}:${baseDir}`, async () => {
let resolve = async () => {
let pkgDir: string | null = null
let mod: any | null = null

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

return [mod, pkgDir] as const
})
return [mod, pkgDir] as [any | null, string | null]
}

let result = await resolve()

// Cache all directories from baseDir up to package location
let [, pkgDir] = result
if (pkgDir) {
cacheForDirs(resolvedModCache, baseDir, result, pkgDir, makeKey)
} else {
resolvedModCache.set(makeKey(baseDir), result)
}

return result
}

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

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

if (configPath === undefined) {
try {
let foundPath = escalade(inputDir, (_, names) => {
if (names.includes('tailwind.config.js')) return 'tailwind.config.js'
if (names.includes('tailwind.config.cjs')) return 'tailwind.config.cjs'
if (names.includes('tailwind.config.mjs')) return 'tailwind.config.mjs'
if (names.includes('tailwind.config.ts')) return 'tailwind.config.ts'
})

configPath = foundPath ?? null
} catch {}
function findClosestJsConfig(inputDir: string): string | null {
// Check cache for this directory
let cached = configPathCache.get(inputDir)
if (cached !== undefined) {
return cached
}

configPath ??= null
configPathCache.set(inputDir, configPath)
// Resolve
let configPath: string | null = null
try {
let foundPath = escalade(inputDir, (_, names) => {
if (names.includes('tailwind.config.js')) return 'tailwind.config.js'
if (names.includes('tailwind.config.cjs')) return 'tailwind.config.cjs'
if (names.includes('tailwind.config.mjs')) return 'tailwind.config.mjs'
if (names.includes('tailwind.config.ts')) return 'tailwind.config.ts'
})
configPath = foundPath ?? null
} catch {}

// Cache all directories from inputDir up to config location
if (configPath) {
cacheForDirs(configPathCache, inputDir, configPath, path.dirname(configPath))
} else {
configPathCache.set(inputDir, null)
}

return configPath
Expand Down