diff --git a/.changeset/calm-paths-agree.md b/.changeset/calm-paths-agree.md new file mode 100644 index 00000000..3bf24900 --- /dev/null +++ b/.changeset/calm-paths-agree.md @@ -0,0 +1,5 @@ +--- +"leadtype": patch +--- + +Fix snippet typechecking on Windows by normalizing virtual file paths before TypeScript compiler host lookups. diff --git a/packages/leadtype/src/lint/snippet-typecheck.test.ts b/packages/leadtype/src/lint/snippet-typecheck.test.ts index 789343a0..fb3fde7a 100644 --- a/packages/leadtype/src/lint/snippet-typecheck.test.ts +++ b/packages/leadtype/src/lint/snippet-typecheck.test.ts @@ -3,6 +3,7 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { lintDocs } from "./runner"; +import { toPosixPath } from "./snippet-typecheck"; const tempDirs: string[] = []; @@ -188,4 +189,40 @@ describe("snippet typechecking", () => { ); expect(violations).toEqual([]); }); + + it("catches typecheck errors when projectRoot contains Windows-style backslashes (Windows regression integration test)", async () => { + const { projectRoot, srcDir } = await createTypecheckProject(); + + // Create a path containing backslashes to simulate a Windows environment path separator mismatch + const backslashedProjectRoot = `${projectRoot}${path.sep}subdir\\subsubdir`; + + const violations = await lintWithTypecheck( + backslashedProjectRoot, + srcDir, + [ + "```ts", + "// @check", + 'const x: number = "this is a string";', + "```", + "", + ].join("\n") + ); + + // This should successfully detect the type error + expect(violations).toEqual([ + expect.objectContaining({ + rule: "snippet:types", + severity: "error", + message: expect.stringContaining("string"), + }), + ]); + }); + + describe("toPosixPath", () => { + it("converts Windows backslash paths to POSIX forward slash paths", () => { + expect( + toPosixPath("C:\\project\\root\\.leadtype-snippet-0\\main.ts") + ).toBe("C:/project/root/.leadtype-snippet-0/main.ts"); + }); + }); }); diff --git a/packages/leadtype/src/lint/snippet-typecheck.ts b/packages/leadtype/src/lint/snippet-typecheck.ts index 6af71669..49268ab3 100644 --- a/packages/leadtype/src/lint/snippet-typecheck.ts +++ b/packages/leadtype/src/lint/snippet-typecheck.ts @@ -217,6 +217,10 @@ function isUnresolvedPackageImport( return !(specifier.startsWith(".") || specifier.startsWith("/")); } +export function toPosixPath(filePath: string): string { + return filePath.replace(/\\/g, "/"); +} + export function typecheckSnippets( options: TypecheckSnippetsOptions ): SnippetTypecheckIssue[] { @@ -229,7 +233,7 @@ export function typecheckSnippets( for (const [index, snippet] of options.snippets.entries()) { const virtualDir = `${projectRoot}/.leadtype-snippet-${index}`; for (const file of toVirtualFiles(snippet, virtualDir)) { - virtualFiles.set(file.path, file); + virtualFiles.set(toPosixPath(file.path), file); } } @@ -245,16 +249,16 @@ export function typecheckSnippets( const baseGetSourceFile = host.getSourceFile.bind(host); const baseDirectoryExists = host.directoryExists?.bind(host); host.fileExists = (fileName) => - virtualFiles.has(fileName) || baseFileExists(fileName); + virtualFiles.has(toPosixPath(fileName)) || baseFileExists(fileName); host.readFile = (fileName) => - virtualFiles.get(fileName)?.content ?? baseReadFile(fileName); + virtualFiles.get(toPosixPath(fileName))?.content ?? baseReadFile(fileName); // Module resolution probes the containing directory; virtual dirs never // exist on disk, so sibling imports (`./helpers`) need this to resolve. host.directoryExists = (directoryName) => - virtualDirs.has(directoryName) || + virtualDirs.has(toPosixPath(directoryName)) || (baseDirectoryExists?.(directoryName) ?? false); host.getSourceFile = (fileName, languageVersion, ...rest) => { - const virtual = virtualFiles.get(fileName); + const virtual = virtualFiles.get(toPosixPath(fileName)); if (virtual) { return ts.createSourceFile(fileName, virtual.content, languageVersion); } @@ -287,7 +291,9 @@ export function typecheckSnippets( continue; } const fileName = diagnostic.file?.fileName; - const virtual = fileName ? virtualFiles.get(fileName) : undefined; + const virtual = fileName + ? virtualFiles.get(toPosixPath(fileName)) + : undefined; if (!virtual) { continue; // diagnostics from real project files aren't lint's business }