Unverified 提交 cb7366c4 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Fix Windows backslash paths in component taggers and visual editing (#2280)

## Summary - Fixes Windows-specific bug where `path.relative()` generates backslash paths (e.g., `src\components\Button.tsx`) that break TSC workers and git operations - Normalizes paths to use forward slashes in both component tagger plugins (nextjs-webpack and react-vite) - Adds defensive normalization in `visual_editing_handlers.ts` and `PreviewIframe.tsx` Fixes #2271 ## Test plan - [ ] Test on Windows: use Annotator / Edit with Select to refactor or move files - [ ] Verify no TSC worker errors occur after using the visual editing tools - [ ] Verify git operations don't fail due to invalid paths 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Bug fix: Windows path normalization** > > - Update `safeJoin` to normalize backslashes via `normalizePath`, then validate/block absolute, home, and UNC paths before joining; join uses normalized segments > - Normalize `gitAdd` filepaths for both native Git and isomorphic-git; adjust error messages accordingly > - Visual editing handler now stages modified files with `gitAdd` (replacing direct isomorphic-git add) while continuing to use `safeJoin` > - Tests updated to assert forward-slash normalization for Windows-style inputs > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5e1cf28e5632a37c564c71b52cc702b7c57e49fc. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Normalize Windows backslash paths to forward slashes across component taggers, visual editing, and git staging so TSC workers and git operations don’t break on Windows. Centralizes normalization in gitAdd and safeJoin for consistent component IDs and file paths. - **Bug Fixes** - Next.js webpack and Vite taggers: replace backslashes from path.relative() with forward slashes. - Git utils: gitAdd normalizes filepath before staging (native Git and isomorphic-git). - Path utils: safeJoin normalizes segments before joining and blocks unsafe absolute paths. - Visual editing and preview: normalize component IDs and paths; use gitAdd and safeJoin for file staging. <sup>Written for commit 674f783cdd844fb98cd746cd8d7dc9234d419d80. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: 's avatarClaude Opus 4.5 <noreply@anthropic.com>
上级 dc33f678
...@@ -50,16 +50,14 @@ describe("safeJoin", () => { ...@@ -50,16 +50,14 @@ describe("safeJoin", () => {
it("should handle Windows-style relative paths with backslashes", () => { it("should handle Windows-style relative paths with backslashes", () => {
const result = safeJoin(testBaseDir, "src\\components\\Button.tsx"); const result = safeJoin(testBaseDir, "src\\components\\Button.tsx");
expect(result).toBe( // safeJoin normalizes backslashes to forward slashes
path.join(testBaseDir, "src\\components\\Button.tsx"), expect(result).toBe("/app/workspace/src/components/Button.tsx");
);
}); });
it("should handle mixed forward/backslashes in relative paths", () => { it("should handle mixed forward/backslashes in relative paths", () => {
const result = safeJoin(testBaseDir, "src/components\\ui/button.tsx"); const result = safeJoin(testBaseDir, "src/components\\ui/button.tsx");
expect(result).toBe( // safeJoin normalizes backslashes to forward slashes
path.join(testBaseDir, "src/components\\ui/button.tsx"), expect(result).toBe("/app/workspace/src/components/ui/button.tsx");
);
}); });
it("should handle Windows-style nested directories", () => { it("should handle Windows-style nested directories", () => {
...@@ -67,14 +65,14 @@ describe("safeJoin", () => { ...@@ -67,14 +65,14 @@ describe("safeJoin", () => {
testBaseDir, testBaseDir,
"pages\\home\\components\\index.tsx", "pages\\home\\components\\index.tsx",
); );
expect(result).toBe( // safeJoin normalizes backslashes to forward slashes
path.join(testBaseDir, "pages\\home\\components\\index.tsx"), expect(result).toBe("/app/workspace/pages/home/components/index.tsx");
);
}); });
it("should handle relative paths starting with dot and backslash", () => { it("should handle relative paths starting with dot and backslash", () => {
const result = safeJoin(testBaseDir, ".\\src\\file.txt"); const result = safeJoin(testBaseDir, ".\\src\\file.txt");
expect(result).toBe(path.join(testBaseDir, ".\\src\\file.txt")); // safeJoin normalizes backslashes to forward slashes
expect(result).toBe("/app/workspace/src/file.txt");
}); });
}); });
...@@ -174,19 +172,19 @@ describe("safeJoin", () => { ...@@ -174,19 +172,19 @@ describe("safeJoin", () => {
it("should allow Windows-style paths that look like drive letters but aren't", () => { it("should allow Windows-style paths that look like drive letters but aren't", () => {
// These look like they could be problematic but are actually safe relative paths // These look like they could be problematic but are actually safe relative paths
// safeJoin normalizes backslashes to forward slashes
const result1 = safeJoin(testBaseDir, "C_drive\\file.txt"); const result1 = safeJoin(testBaseDir, "C_drive\\file.txt");
expect(result1).toBe(path.join(testBaseDir, "C_drive\\file.txt")); expect(result1).toBe("/app/workspace/C_drive/file.txt");
const result2 = safeJoin(testBaseDir, "src\\C-file.txt"); const result2 = safeJoin(testBaseDir, "src\\C-file.txt");
expect(result2).toBe(path.join(testBaseDir, "src\\C-file.txt")); expect(result2).toBe("/app/workspace/src/C-file.txt");
}); });
it("should handle Windows paths with multiple backslashes (not UNC)", () => { it("should handle Windows paths with multiple backslashes (not UNC)", () => {
// Single backslashes in the middle are fine - it's only \\ at the start that's UNC // Single backslashes in the middle are fine - it's only \\ at the start that's UNC
// safeJoin normalizes backslashes to forward slashes
const result = safeJoin(testBaseDir, "src\\\\components\\\\Button.tsx"); const result = safeJoin(testBaseDir, "src\\\\components\\\\Button.tsx");
expect(result).toBe( expect(result).toBe("/app/workspace/src/components/Button.tsx");
path.join(testBaseDir, "src\\\\components\\\\Button.tsx"),
);
}); });
it("should provide descriptive error messages", () => { it("should provide descriptive error messages", () => {
......
...@@ -321,18 +321,19 @@ export async function gitAddAll({ path }: GitBaseParams): Promise<void> { ...@@ -321,18 +321,19 @@ export async function gitAddAll({ path }: GitBaseParams): Promise<void> {
} }
export async function gitAdd({ path, filepath }: GitFileParams): Promise<void> { export async function gitAdd({ path, filepath }: GitFileParams): Promise<void> {
const normalizedFilepath = normalizePath(filepath);
const settings = readSettings(); const settings = readSettings();
if (settings.enableNativeGit) { if (settings.enableNativeGit) {
await execOrThrow( await execOrThrow(
["add", "--", filepath], ["add", "--", normalizedFilepath],
path, path,
`Failed to stage file '${filepath}'`, `Failed to stage file '${normalizedFilepath}'`,
); );
} else { } else {
await git.add({ await git.add({
fs, fs,
dir: path, dir: path,
filepath, filepath: normalizedFilepath,
}); });
} }
} }
......
import path from "node:path"; import path from "node:path";
import { normalizePath } from "../../../shared/normalizePath";
/** /**
* Safely joins paths while ensuring the result stays within the base directory. * Safely joins paths while ensuring the result stays within the base directory.
...@@ -11,8 +12,11 @@ import path from "node:path"; ...@@ -11,8 +12,11 @@ import path from "node:path";
* @throws Error if the resulting path would be outside the base directory * @throws Error if the resulting path would be outside the base directory
*/ */
export function safeJoin(basePath: string, ...paths: string[]): string { export function safeJoin(basePath: string, ...paths: string[]): string {
// Normalize backslashes to forward slashes for cross-platform consistency
const normalizedPaths = paths.map((p) => normalizePath(p));
// Check if any of the path segments are absolute paths (which would be unsafe) // Check if any of the path segments are absolute paths (which would be unsafe)
for (const pathSegment of paths) { for (const pathSegment of normalizedPaths) {
if (path.isAbsolute(pathSegment)) { if (path.isAbsolute(pathSegment)) {
throw new Error( throw new Error(
`Unsafe path: joining "${paths.join(", ")}" with base "${basePath}" would escape the base directory`, `Unsafe path: joining "${paths.join(", ")}" with base "${basePath}" would escape the base directory`,
...@@ -39,7 +43,7 @@ export function safeJoin(basePath: string, ...paths: string[]): string { ...@@ -39,7 +43,7 @@ export function safeJoin(basePath: string, ...paths: string[]): string {
} }
// Join all the paths // Join all the paths
const joinedPath = path.join(basePath, ...paths); const joinedPath = path.join(basePath, ...normalizedPaths);
// Resolve both paths to absolute paths to handle any ".." components // Resolve both paths to absolute paths to handle any ".." components
const resolvedBasePath = path.resolve(basePath); const resolvedBasePath = path.resolve(basePath);
......
...@@ -10,8 +10,7 @@ import { ...@@ -10,8 +10,7 @@ import {
stylesToTailwind, stylesToTailwind,
extractClassPrefixes, extractClassPrefixes,
} from "../../../../utils/style-utils"; } from "../../../../utils/style-utils";
import git from "isomorphic-git"; import { gitAdd, gitCommit } from "../../../../ipc/utils/git_utils";
import { gitCommit } from "../../../../ipc/utils/git_utils";
import { safeJoin } from "@/ipc/utils/path_utils"; import { safeJoin } from "@/ipc/utils/path_utils";
import { import {
AnalyseComponentParams, AnalyseComponentParams,
...@@ -21,6 +20,7 @@ import { ...@@ -21,6 +20,7 @@ import {
transformContent, transformContent,
analyzeComponent, analyzeComponent,
} from "../../utils/visual_editing_utils"; } from "../../utils/visual_editing_utils";
import { normalizePath } from "../../../../../shared/normalizePath";
export function registerVisualEditingHandlers() { export function registerVisualEditingHandlers() {
ipcMain.handle( ipcMain.handle(
...@@ -67,21 +67,21 @@ export function registerVisualEditingHandlers() { ...@@ -67,21 +67,21 @@ export function registerVisualEditingHandlers() {
// Apply changes to each file // Apply changes to each file
for (const [relativePath, lineChanges] of fileChanges) { for (const [relativePath, lineChanges] of fileChanges) {
const filePath = safeJoin(appPath, relativePath); const normalizedRelativePath = normalizePath(relativePath);
const filePath = safeJoin(appPath, normalizedRelativePath);
const content = await fsPromises.readFile(filePath, "utf-8"); const content = await fsPromises.readFile(filePath, "utf-8");
const transformedContent = transformContent(content, lineChanges); const transformedContent = transformContent(content, lineChanges);
await fsPromises.writeFile(filePath, transformedContent, "utf-8"); await fsPromises.writeFile(filePath, transformedContent, "utf-8");
// Check if git repository exists and commit the change // Check if git repository exists and commit the change
if (fs.existsSync(path.join(appPath, ".git"))) { if (fs.existsSync(path.join(appPath, ".git"))) {
await git.add({ await gitAdd({
fs, path: appPath,
dir: appPath, filepath: normalizedRelativePath,
filepath: relativePath,
}); });
await gitCommit({ await gitCommit({
path: appPath, path: appPath,
message: `Updated ${relativePath}`, message: `Updated ${normalizedRelativePath}`,
}); });
} }
} }
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论