Merge pull request #2804 from code-yeongyu/fix/b2-hashline-formatter-cache-per-project
fix(hashline-edit): scope formatter cache by directory
This commit is contained in:
@@ -0,0 +1,117 @@
|
|||||||
|
import { beforeEach, describe, expect, it, mock } from "bun:test"
|
||||||
|
|
||||||
|
import { clearFormatterCache, resolveFormatters, type FormatterClient } from "./formatter-trigger"
|
||||||
|
|
||||||
|
function createDirectoryAwareClient(
|
||||||
|
resolveConfig: (directory: string) => Promise<Record<string, unknown> | undefined>,
|
||||||
|
): FormatterClient {
|
||||||
|
return {
|
||||||
|
config: {
|
||||||
|
get: mock(async ({ query }: { query?: { directory?: string } } = {}) => ({
|
||||||
|
data: await resolveConfig(query?.directory ?? ""),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("resolveFormatters cache behavior", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
clearFormatterCache()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("caches formatter resolution per directory", async () => {
|
||||||
|
//#given
|
||||||
|
const client = createDirectoryAwareClient(async (directory) => {
|
||||||
|
if (directory === "/project-a") {
|
||||||
|
return {
|
||||||
|
formatter: {
|
||||||
|
prettier: {
|
||||||
|
command: ["prettier", "--write", "$FILE"],
|
||||||
|
extensions: [".ts"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
formatter: {
|
||||||
|
biome: {
|
||||||
|
command: ["biome", "format", "$FILE"],
|
||||||
|
extensions: [".ts"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const firstProjectAResult = await resolveFormatters(client, "/project-a")
|
||||||
|
const projectBResult = await resolveFormatters(client, "/project-b")
|
||||||
|
const secondProjectAResult = await resolveFormatters(client, "/project-a")
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(client.config.get).toHaveBeenCalledTimes(2)
|
||||||
|
expect(firstProjectAResult.get(".ts")?.[0]?.command).toEqual(["prettier", "--write", "$FILE"])
|
||||||
|
expect(projectBResult.get(".ts")?.[0]?.command).toEqual(["biome", "format", "$FILE"])
|
||||||
|
expect(secondProjectAResult).toBe(firstProjectAResult)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not cache transient config fetch failures", async () => {
|
||||||
|
//#given
|
||||||
|
const get = mock(async () => ({
|
||||||
|
data: {
|
||||||
|
formatter: {
|
||||||
|
prettier: {
|
||||||
|
command: ["prettier", "--write", "$FILE"],
|
||||||
|
extensions: [".ts"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
get.mockImplementationOnce(async () => {
|
||||||
|
throw new Error("network error")
|
||||||
|
})
|
||||||
|
|
||||||
|
const client: FormatterClient = {
|
||||||
|
config: { get },
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const firstResult = await resolveFormatters(client, "/project-a")
|
||||||
|
const secondResult = await resolveFormatters(client, "/project-a")
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(get).toHaveBeenCalledTimes(2)
|
||||||
|
expect(firstResult.size).toBe(0)
|
||||||
|
expect(secondResult.get(".ts")?.[0]?.command).toEqual(["prettier", "--write", "$FILE"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not cache missing config data", async () => {
|
||||||
|
//#given
|
||||||
|
let callCount = 0
|
||||||
|
const client = createDirectoryAwareClient(async () => {
|
||||||
|
callCount += 1
|
||||||
|
if (callCount === 1) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
formatter: {
|
||||||
|
prettier: {
|
||||||
|
command: ["prettier", "--write", "$FILE"],
|
||||||
|
extensions: [".ts"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const firstResult = await resolveFormatters(client, "/project-a")
|
||||||
|
const secondResult = await resolveFormatters(client, "/project-a")
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(client.config.get).toHaveBeenCalledTimes(2)
|
||||||
|
expect(firstResult.size).toBe(0)
|
||||||
|
expect(secondResult.get(".ts")?.[0]?.command).toEqual(["prettier", "--write", "$FILE"])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -25,15 +25,24 @@ export interface FormatterClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let cachedFormatters: Map<string, Array<{ command: string[]; environment: Record<string, string> }>> | null = null
|
type FormatterDefinition = { command: string[]; environment: Record<string, string> }
|
||||||
|
type FormatterMap = Map<string, FormatterDefinition[]>
|
||||||
|
|
||||||
|
const cachedFormattersByDirectory = new Map<string, FormatterMap>()
|
||||||
|
|
||||||
|
function getFormatterCacheKey(directory: string): string {
|
||||||
|
return path.resolve(directory)
|
||||||
|
}
|
||||||
|
|
||||||
export async function resolveFormatters(
|
export async function resolveFormatters(
|
||||||
client: FormatterClient,
|
client: FormatterClient,
|
||||||
directory: string,
|
directory: string,
|
||||||
): Promise<Map<string, Array<{ command: string[]; environment: Record<string, string> }>>> {
|
): Promise<FormatterMap> {
|
||||||
|
const cacheKey = getFormatterCacheKey(directory)
|
||||||
|
const cachedFormatters = cachedFormattersByDirectory.get(cacheKey)
|
||||||
if (cachedFormatters) return cachedFormatters
|
if (cachedFormatters) return cachedFormatters
|
||||||
|
|
||||||
const result = new Map<string, Array<{ command: string[]; environment: Record<string, string> }>>()
|
const result = new Map<string, FormatterDefinition[]>()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await client.config.get({ query: { directory } })
|
const response = await client.config.get({ query: { directory } })
|
||||||
@@ -68,11 +77,12 @@ export async function resolveFormatters(
|
|||||||
result.set(normalizedExt, existing)
|
result.set(normalizedExt, existing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cachedFormattersByDirectory.set(cacheKey, result)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log("[formatter-trigger] Failed to fetch formatter config", { error })
|
log("[formatter-trigger] Failed to fetch formatter config", { error })
|
||||||
}
|
}
|
||||||
|
|
||||||
cachedFormatters = result
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,5 +128,5 @@ export async function runFormattersForFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function clearFormatterCache(): void {
|
export function clearFormatterCache(): void {
|
||||||
cachedFormatters = null
|
cachedFormattersByDirectory.clear()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user