Normalize vfs (#558)
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import { test, testSkipIfWindows } from "./helpers/test_helper";
|
||||
import { test } from "./helpers/test_helper";
|
||||
import { expect } from "@playwright/test";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const MINIMAL_APP = "minimal-with-ai-rules";
|
||||
|
||||
testSkipIfWindows("problems auto-fix - enabled", async ({ po }) => {
|
||||
test("problems auto-fix - enabled", async ({ po }) => {
|
||||
await po.setUp();
|
||||
await po.importApp(MINIMAL_APP);
|
||||
await po.expectPreviewIframeIsVisible();
|
||||
@@ -18,9 +18,7 @@ testSkipIfWindows("problems auto-fix - enabled", async ({ po }) => {
|
||||
await po.snapshotMessages({ replaceDumpPath: true });
|
||||
});
|
||||
|
||||
testSkipIfWindows(
|
||||
"problems auto-fix - gives up after 2 attempts",
|
||||
async ({ po }) => {
|
||||
test("problems auto-fix - gives up after 2 attempts", async ({ po }) => {
|
||||
await po.setUp();
|
||||
await po.importApp(MINIMAL_APP);
|
||||
await po.expectPreviewIframeIsVisible();
|
||||
@@ -35,12 +33,9 @@ testSkipIfWindows(
|
||||
po.page.getByTestId("problem-summary").last(),
|
||||
).toMatchAriaSnapshot();
|
||||
await po.snapshotMessages({ replaceDumpPath: true });
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
testSkipIfWindows(
|
||||
"problems auto-fix - complex delete-rename-write",
|
||||
async ({ po }) => {
|
||||
test("problems auto-fix - complex delete-rename-write", async ({ po }) => {
|
||||
await po.setUp();
|
||||
await po.importApp(MINIMAL_APP);
|
||||
await po.expectPreviewIframeIsVisible();
|
||||
@@ -51,8 +46,7 @@ testSkipIfWindows(
|
||||
await po.snapshotServerDump("all-messages", { dumpIndex: -1 });
|
||||
|
||||
await po.snapshotMessages({ replaceDumpPath: true });
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("problems auto-fix - disabled", async ({ po }) => {
|
||||
await po.setUp({ disableAutoFixProblems: true });
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
getDyadRenameTags,
|
||||
getDyadDeleteTags,
|
||||
} from "../ipc/processors/response_processor";
|
||||
import { normalizePath } from "../ipc/processors/normalizePath";
|
||||
|
||||
import log from "electron-log";
|
||||
|
||||
@@ -39,7 +40,39 @@ export abstract class BaseVirtualFileSystem {
|
||||
protected baseDir: string;
|
||||
|
||||
constructor(baseDir: string) {
|
||||
this.baseDir = baseDir;
|
||||
this.baseDir = path.resolve(baseDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize path for consistent cross-platform behavior
|
||||
*/
|
||||
private normalizePathForKey(filePath: string): string {
|
||||
const absolutePath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.resolve(this.baseDir, filePath);
|
||||
|
||||
// Normalize separators and handle case-insensitive Windows paths
|
||||
const normalized = normalizePath(path.normalize(absolutePath));
|
||||
|
||||
// Intentionally do NOT lowercase for Windows which is case-insensitive
|
||||
// because this avoids issues with path comparison.
|
||||
//
|
||||
// This is a trade-off and introduces a small edge case where
|
||||
// e.g. foo.txt and Foo.txt are treated as different files by the VFS
|
||||
// even though Windows treats them as the same file.
|
||||
//
|
||||
// This should be a pretty rare occurence and it's not worth the extra
|
||||
// complexity to handle it.
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert normalized path back to platform-appropriate format
|
||||
*/
|
||||
private denormalizePath(normalizedPath: string): string {
|
||||
return process.platform === "win32"
|
||||
? normalizedPath.replace(/\//g, "\\")
|
||||
: normalizedPath;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,43 +102,49 @@ export abstract class BaseVirtualFileSystem {
|
||||
/**
|
||||
* Write a file to the virtual filesystem
|
||||
*/
|
||||
public writeFile(relativePath: string, content: string): void {
|
||||
protected writeFile(relativePath: string, content: string): void {
|
||||
const absolutePath = path.resolve(this.baseDir, relativePath);
|
||||
this.virtualFiles.set(absolutePath, content);
|
||||
const normalizedKey = this.normalizePathForKey(absolutePath);
|
||||
|
||||
this.virtualFiles.set(normalizedKey, content);
|
||||
// Remove from deleted files if it was previously deleted
|
||||
this.deletedFiles.delete(absolutePath);
|
||||
this.deletedFiles.delete(normalizedKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a file from the virtual filesystem
|
||||
*/
|
||||
public deleteFile(relativePath: string): void {
|
||||
protected deleteFile(relativePath: string): void {
|
||||
const absolutePath = path.resolve(this.baseDir, relativePath);
|
||||
this.deletedFiles.add(absolutePath);
|
||||
const normalizedKey = this.normalizePathForKey(absolutePath);
|
||||
|
||||
this.deletedFiles.add(normalizedKey);
|
||||
// Remove from virtual files if it exists there
|
||||
this.virtualFiles.delete(absolutePath);
|
||||
this.virtualFiles.delete(normalizedKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a file in the virtual filesystem
|
||||
*/
|
||||
public renameFile(fromPath: string, toPath: string): void {
|
||||
protected renameFile(fromPath: string, toPath: string): void {
|
||||
const fromAbsolute = path.resolve(this.baseDir, fromPath);
|
||||
const toAbsolute = path.resolve(this.baseDir, toPath);
|
||||
const fromNormalized = this.normalizePathForKey(fromAbsolute);
|
||||
const toNormalized = this.normalizePathForKey(toAbsolute);
|
||||
|
||||
// Mark old file as deleted
|
||||
this.deletedFiles.add(fromAbsolute);
|
||||
this.deletedFiles.add(fromNormalized);
|
||||
|
||||
// If the source file exists in virtual files, move its content
|
||||
if (this.virtualFiles.has(fromAbsolute)) {
|
||||
const content = this.virtualFiles.get(fromAbsolute)!;
|
||||
this.virtualFiles.delete(fromAbsolute);
|
||||
this.virtualFiles.set(toAbsolute, content);
|
||||
if (this.virtualFiles.has(fromNormalized)) {
|
||||
const content = this.virtualFiles.get(fromNormalized)!;
|
||||
this.virtualFiles.delete(fromNormalized);
|
||||
this.virtualFiles.set(toNormalized, content);
|
||||
} else {
|
||||
// Try to read from actual filesystem
|
||||
try {
|
||||
const content = fs.readFileSync(fromAbsolute, "utf8");
|
||||
this.virtualFiles.set(toAbsolute, content);
|
||||
this.virtualFiles.set(toNormalized, content);
|
||||
} catch (error) {
|
||||
// If we can't read the source file, we'll let the consumer handle it
|
||||
logger.warn(
|
||||
@@ -116,7 +155,7 @@ export abstract class BaseVirtualFileSystem {
|
||||
}
|
||||
|
||||
// Remove destination from deleted files if it was previously deleted
|
||||
this.deletedFiles.delete(toAbsolute);
|
||||
this.deletedFiles.delete(toNormalized);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,10 +163,15 @@ export abstract class BaseVirtualFileSystem {
|
||||
*/
|
||||
public getVirtualFiles(): VirtualFile[] {
|
||||
return Array.from(this.virtualFiles.entries()).map(
|
||||
([absolutePath, content]) => ({
|
||||
path: path.relative(this.baseDir, absolutePath),
|
||||
([normalizedKey, content]) => {
|
||||
// Convert normalized key back to relative path
|
||||
const denormalizedPath = this.denormalizePath(normalizedKey);
|
||||
|
||||
return {
|
||||
path: path.relative(this.baseDir, denormalizedPath),
|
||||
content,
|
||||
}),
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -135,84 +179,35 @@ export abstract class BaseVirtualFileSystem {
|
||||
* Get all deleted file paths (relative to base directory)
|
||||
*/
|
||||
public getDeletedFiles(): string[] {
|
||||
return Array.from(this.deletedFiles).map((absolutePath) =>
|
||||
path.relative(this.baseDir, absolutePath),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all files that should be considered (existing + virtual - deleted)
|
||||
*/
|
||||
public getAllFiles(): string[] {
|
||||
const allFiles = new Set<string>();
|
||||
|
||||
// Add virtual files
|
||||
for (const [absolutePath] of this.virtualFiles.entries()) {
|
||||
allFiles.add(path.relative(this.baseDir, absolutePath));
|
||||
}
|
||||
|
||||
// Add existing files (this is a simplified version - in practice you might want to scan the directory)
|
||||
// This method is mainly for getting the current state, consumers can combine with directory scanning
|
||||
|
||||
return Array.from(allFiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file has been modified in the virtual filesystem
|
||||
*/
|
||||
public isFileModified(filePath: string): boolean {
|
||||
const absolutePath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.resolve(this.baseDir, filePath);
|
||||
|
||||
return (
|
||||
this.virtualFiles.has(absolutePath) || this.deletedFiles.has(absolutePath)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all virtual changes
|
||||
*/
|
||||
public clear(): void {
|
||||
this.virtualFiles.clear();
|
||||
this.deletedFiles.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base directory
|
||||
*/
|
||||
public getBaseDir(): string {
|
||||
return this.baseDir;
|
||||
return Array.from(this.deletedFiles).map((normalizedKey) => {
|
||||
// Convert normalized key back to relative path
|
||||
const denormalizedPath = this.denormalizePath(normalizedKey);
|
||||
return path.relative(this.baseDir, denormalizedPath);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file is deleted in the virtual filesystem
|
||||
*/
|
||||
protected isDeleted(filePath: string): boolean {
|
||||
const absolutePath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.resolve(this.baseDir, filePath);
|
||||
return this.deletedFiles.has(absolutePath);
|
||||
const normalizedKey = this.normalizePathForKey(filePath);
|
||||
return this.deletedFiles.has(normalizedKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file exists in virtual files
|
||||
*/
|
||||
protected hasVirtualFile(filePath: string): boolean {
|
||||
const absolutePath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.resolve(this.baseDir, filePath);
|
||||
return this.virtualFiles.has(absolutePath);
|
||||
const normalizedKey = this.normalizePathForKey(filePath);
|
||||
return this.virtualFiles.has(normalizedKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get virtual file content
|
||||
*/
|
||||
protected getVirtualFileContent(filePath: string): string | undefined {
|
||||
const absolutePath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.resolve(this.baseDir, filePath);
|
||||
return this.virtualFiles.get(absolutePath);
|
||||
const normalizedKey = this.normalizePathForKey(filePath);
|
||||
return this.virtualFiles.get(normalizedKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user