From 74afb4ca69c5cae48f3dcebfdfe28843a2077a71 Mon Sep 17 00:00:00 2001 From: RunMintOn <1639562902@qq.com> Date: Fri, 17 Jul 2026 20:07:25 +0800 Subject: [PATCH] fix(pi-fff): handle Windows cross-volume external paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows, `path.relative()` returns an absolute path when the source and target are on different drives (e.g. `D:\` → `C:\`). The existing check in `routePathConstraint()` only recognizes `".."` and `"..\..."` as indicators that a path is outside the workspace — it misses the cross-volume case entirely. The path is then treated as workspace-local, and downstream code rejects it with: Path constraint must be relative to the workspace Extract the workspace-outside check into `isOutsideWorkspaceRelativePath()` so the logic is testable in isolation, and add `path.isAbsolute()` to the condition. A cross-volume relative result is by definition outside the workspace. Add a Windows-specific regression test that verifies the helper recognizes a cross-volume `path.win32.relative()` result as outside the workspace. The test is gated on `process.platform === "win32"` and has no effect on Linux or macOS CI runs. --- packages/pi-fff/src/aux-finders.ts | 10 +++++++++- packages/pi-fff/test/aux-finders.test.ts | 9 +++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/pi-fff/src/aux-finders.ts b/packages/pi-fff/src/aux-finders.ts index 8bec5b75b..6b1fd4713 100644 --- a/packages/pi-fff/src/aux-finders.ts +++ b/packages/pi-fff/src/aux-finders.ts @@ -128,6 +128,14 @@ export function resolveAuxRoot( return null; } +export function isOutsideWorkspaceRelativePath(relativePath: string): boolean { + return ( + path.isAbsolute(relativePath) || + relativePath === ".." || + relativePath.startsWith(`..${path.sep}`) + ); +} + // Decide whether a `path` parameter should route to the workspace finder or // to an aux finder. Accepts absolute paths, `~`-prefixed paths, and relative // paths escaping the workspace (`../other-project`); everything is resolved @@ -147,7 +155,7 @@ export function routePathConstraint( candidate = path.resolve(cwd, candidate); } const rel = path.relative(cwd, candidate); - if (rel !== ".." && !rel.startsWith(`..${path.sep}`)) return null; + if (!isOutsideWorkspaceRelativePath(rel)) return null; return resolveAuxRoot(candidate); } diff --git a/packages/pi-fff/test/aux-finders.test.ts b/packages/pi-fff/test/aux-finders.test.ts index 0b829bf92..7237aa030 100644 --- a/packages/pi-fff/test/aux-finders.test.ts +++ b/packages/pi-fff/test/aux-finders.test.ts @@ -3,6 +3,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { + isOutsideWorkspaceRelativePath, resolveAuxRoot, rootCovers, routePathConstraint, @@ -28,6 +29,14 @@ describe("routePathConstraint", () => { expect(route).toEqual({ root: "/tmp", suffix: "" }); }); + if (process.platform === "win32") { + test("treats a cross-volume relative result as outside the workspace", () => { + const rel = path.win32.relative("D:\\workspace", "C:\\target"); + expect(rel).toBe("C:\\target"); + expect(isOutsideWorkspaceRelativePath(rel)).toBe(true); + }); + } + test("splits glob suffix from existing dir prefix", () => { const route = routePathConstraint("/tmp/**/*.ts", cwd); expect(route).toEqual({ root: "/tmp", suffix: "**/*.ts" });