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 { expect } from "@playwright/test";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
const MINIMAL_APP = "minimal-with-ai-rules";
|
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.setUp();
|
||||||
await po.importApp(MINIMAL_APP);
|
await po.importApp(MINIMAL_APP);
|
||||||
await po.expectPreviewIframeIsVisible();
|
await po.expectPreviewIframeIsVisible();
|
||||||
@@ -18,9 +18,7 @@ testSkipIfWindows("problems auto-fix - enabled", async ({ po }) => {
|
|||||||
await po.snapshotMessages({ replaceDumpPath: true });
|
await po.snapshotMessages({ replaceDumpPath: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
testSkipIfWindows(
|
test("problems auto-fix - gives up after 2 attempts", async ({ po }) => {
|
||||||
"problems auto-fix - gives up after 2 attempts",
|
|
||||||
async ({ po }) => {
|
|
||||||
await po.setUp();
|
await po.setUp();
|
||||||
await po.importApp(MINIMAL_APP);
|
await po.importApp(MINIMAL_APP);
|
||||||
await po.expectPreviewIframeIsVisible();
|
await po.expectPreviewIframeIsVisible();
|
||||||
@@ -35,12 +33,9 @@ testSkipIfWindows(
|
|||||||
po.page.getByTestId("problem-summary").last(),
|
po.page.getByTestId("problem-summary").last(),
|
||||||
).toMatchAriaSnapshot();
|
).toMatchAriaSnapshot();
|
||||||
await po.snapshotMessages({ replaceDumpPath: true });
|
await po.snapshotMessages({ replaceDumpPath: true });
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
testSkipIfWindows(
|
test("problems auto-fix - complex delete-rename-write", async ({ po }) => {
|
||||||
"problems auto-fix - complex delete-rename-write",
|
|
||||||
async ({ po }) => {
|
|
||||||
await po.setUp();
|
await po.setUp();
|
||||||
await po.importApp(MINIMAL_APP);
|
await po.importApp(MINIMAL_APP);
|
||||||
await po.expectPreviewIframeIsVisible();
|
await po.expectPreviewIframeIsVisible();
|
||||||
@@ -51,8 +46,7 @@ testSkipIfWindows(
|
|||||||
await po.snapshotServerDump("all-messages", { dumpIndex: -1 });
|
await po.snapshotServerDump("all-messages", { dumpIndex: -1 });
|
||||||
|
|
||||||
await po.snapshotMessages({ replaceDumpPath: true });
|
await po.snapshotMessages({ replaceDumpPath: true });
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
test("problems auto-fix - disabled", async ({ po }) => {
|
test("problems auto-fix - disabled", async ({ po }) => {
|
||||||
await po.setUp({ disableAutoFixProblems: true });
|
await po.setUp({ disableAutoFixProblems: true });
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
getDyadRenameTags,
|
getDyadRenameTags,
|
||||||
getDyadDeleteTags,
|
getDyadDeleteTags,
|
||||||
} from "../ipc/processors/response_processor";
|
} from "../ipc/processors/response_processor";
|
||||||
|
import { normalizePath } from "../ipc/processors/normalizePath";
|
||||||
|
|
||||||
import log from "electron-log";
|
import log from "electron-log";
|
||||||
|
|
||||||
@@ -39,7 +40,39 @@ export abstract class BaseVirtualFileSystem {
|
|||||||
protected baseDir: string;
|
protected baseDir: string;
|
||||||
|
|
||||||
constructor(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
|
* 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);
|
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
|
// Remove from deleted files if it was previously deleted
|
||||||
this.deletedFiles.delete(absolutePath);
|
this.deletedFiles.delete(normalizedKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a file from the virtual filesystem
|
* Delete a file from the virtual filesystem
|
||||||
*/
|
*/
|
||||||
public deleteFile(relativePath: string): void {
|
protected deleteFile(relativePath: string): void {
|
||||||
const absolutePath = path.resolve(this.baseDir, relativePath);
|
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
|
// Remove from virtual files if it exists there
|
||||||
this.virtualFiles.delete(absolutePath);
|
this.virtualFiles.delete(normalizedKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rename a file in the virtual filesystem
|
* 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 fromAbsolute = path.resolve(this.baseDir, fromPath);
|
||||||
const toAbsolute = path.resolve(this.baseDir, toPath);
|
const toAbsolute = path.resolve(this.baseDir, toPath);
|
||||||
|
const fromNormalized = this.normalizePathForKey(fromAbsolute);
|
||||||
|
const toNormalized = this.normalizePathForKey(toAbsolute);
|
||||||
|
|
||||||
// Mark old file as deleted
|
// 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 the source file exists in virtual files, move its content
|
||||||
if (this.virtualFiles.has(fromAbsolute)) {
|
if (this.virtualFiles.has(fromNormalized)) {
|
||||||
const content = this.virtualFiles.get(fromAbsolute)!;
|
const content = this.virtualFiles.get(fromNormalized)!;
|
||||||
this.virtualFiles.delete(fromAbsolute);
|
this.virtualFiles.delete(fromNormalized);
|
||||||
this.virtualFiles.set(toAbsolute, content);
|
this.virtualFiles.set(toNormalized, content);
|
||||||
} else {
|
} else {
|
||||||
// Try to read from actual filesystem
|
// Try to read from actual filesystem
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(fromAbsolute, "utf8");
|
const content = fs.readFileSync(fromAbsolute, "utf8");
|
||||||
this.virtualFiles.set(toAbsolute, content);
|
this.virtualFiles.set(toNormalized, content);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If we can't read the source file, we'll let the consumer handle it
|
// If we can't read the source file, we'll let the consumer handle it
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -116,7 +155,7 @@ export abstract class BaseVirtualFileSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove destination from deleted files if it was previously deleted
|
// 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[] {
|
public getVirtualFiles(): VirtualFile[] {
|
||||||
return Array.from(this.virtualFiles.entries()).map(
|
return Array.from(this.virtualFiles.entries()).map(
|
||||||
([absolutePath, content]) => ({
|
([normalizedKey, content]) => {
|
||||||
path: path.relative(this.baseDir, absolutePath),
|
// Convert normalized key back to relative path
|
||||||
|
const denormalizedPath = this.denormalizePath(normalizedKey);
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: path.relative(this.baseDir, denormalizedPath),
|
||||||
content,
|
content,
|
||||||
}),
|
};
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,84 +179,35 @@ export abstract class BaseVirtualFileSystem {
|
|||||||
* Get all deleted file paths (relative to base directory)
|
* Get all deleted file paths (relative to base directory)
|
||||||
*/
|
*/
|
||||||
public getDeletedFiles(): string[] {
|
public getDeletedFiles(): string[] {
|
||||||
return Array.from(this.deletedFiles).map((absolutePath) =>
|
return Array.from(this.deletedFiles).map((normalizedKey) => {
|
||||||
path.relative(this.baseDir, absolutePath),
|
// Convert normalized key back to relative path
|
||||||
);
|
const denormalizedPath = this.denormalizePath(normalizedKey);
|
||||||
}
|
return path.relative(this.baseDir, denormalizedPath);
|
||||||
|
});
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a file is deleted in the virtual filesystem
|
* Check if a file is deleted in the virtual filesystem
|
||||||
*/
|
*/
|
||||||
protected isDeleted(filePath: string): boolean {
|
protected isDeleted(filePath: string): boolean {
|
||||||
const absolutePath = path.isAbsolute(filePath)
|
const normalizedKey = this.normalizePathForKey(filePath);
|
||||||
? filePath
|
return this.deletedFiles.has(normalizedKey);
|
||||||
: path.resolve(this.baseDir, filePath);
|
|
||||||
return this.deletedFiles.has(absolutePath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a file exists in virtual files
|
* Check if a file exists in virtual files
|
||||||
*/
|
*/
|
||||||
protected hasVirtualFile(filePath: string): boolean {
|
protected hasVirtualFile(filePath: string): boolean {
|
||||||
const absolutePath = path.isAbsolute(filePath)
|
const normalizedKey = this.normalizePathForKey(filePath);
|
||||||
? filePath
|
return this.virtualFiles.has(normalizedKey);
|
||||||
: path.resolve(this.baseDir, filePath);
|
|
||||||
return this.virtualFiles.has(absolutePath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get virtual file content
|
* Get virtual file content
|
||||||
*/
|
*/
|
||||||
protected getVirtualFileContent(filePath: string): string | undefined {
|
protected getVirtualFileContent(filePath: string): string | undefined {
|
||||||
const absolutePath = path.isAbsolute(filePath)
|
const normalizedKey = this.normalizePathForKey(filePath);
|
||||||
? filePath
|
return this.virtualFiles.get(normalizedKey);
|
||||||
: path.resolve(this.baseDir, filePath);
|
|
||||||
return this.virtualFiles.get(absolutePath);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user