Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-win32-relative-paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"enhanced-resolve": patch
---

Recognize win32 relative paths (e.g. `..\src`) in `getType` and `isRelativeRequest`. On Windows, `path.relative()` returns backslash-separated paths that were previously misclassified as bare specifiers, causing resolution to fail. Backslashes in relative paths are now normalized to forward slashes internally.
24 changes: 19 additions & 5 deletions lib/util/path.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ const CHAR_QUESTION = "?".charCodeAt(0);
const posixNormalize = path.posix.normalize;
const winNormalize = path.win32.normalize;

const BACKSLASH_RE = /\\/g;

/**
* @param {string} str path that may contain backslashes
* @returns {string} path with backslashes replaced by forward slashes
*/
const toPosixSep = (str) =>
str.includes("\\") ? str.replace(BACKSLASH_RE, "/") : str;

/**
* @enum {number}
*/
Expand Down Expand Up @@ -81,6 +90,7 @@ const getType = (maybePath) => {
switch (c1) {
case CHAR_DOT:
case CHAR_SLASH:
case CHAR_BACKSLASH:
return PathType.Relative;
}
return PathType.Normal;
Expand All @@ -107,10 +117,13 @@ const getType = (maybePath) => {
const c1 = maybePath.charCodeAt(1);
switch (c1) {
case CHAR_SLASH:
case CHAR_BACKSLASH:
return PathType.Relative;
case CHAR_DOT: {
const c2 = maybePath.charCodeAt(2);
if (c2 === CHAR_SLASH) return PathType.Relative;
if (c2 === CHAR_SLASH || c2 === CHAR_BACKSLASH) {
return PathType.Relative;
}
return PathType.Normal;
}
}
Expand Down Expand Up @@ -153,7 +166,7 @@ const normalize = (maybePath) => {
case PathType.AbsoluteWin:
return winNormalize(maybePath);
case PathType.Relative: {
const r = posixNormalize(maybePath);
const r = posixNormalize(toPosixSep(maybePath));
return getType(r) === PathType.Relative ? r : `./${r}`;
}
}
Expand All @@ -178,7 +191,7 @@ const join = (rootPath, request) => {
case PathType.Normal:
case PathType.Relative:
case PathType.AbsolutePosix:
return posixNormalize(`${rootPath}/${request}`);
return posixNormalize(`${rootPath}/${toPosixSep(request)}`);
case PathType.AbsoluteWin:
return winNormalize(`${rootPath}\\${request}`);
}
Expand Down Expand Up @@ -290,10 +303,11 @@ const isRelativeRequest = (request) => {
if (len === 0 || request.charCodeAt(0) !== CHAR_DOT) return false;
if (len === 1) return true; // "."
const c1 = request.charCodeAt(1);
if (c1 === CHAR_SLASH) return true; // "./..."
if (c1 === CHAR_SLASH || c1 === CHAR_BACKSLASH) return true; // "./..." or ".\\..."
if (c1 !== CHAR_DOT) return false; // ".x..."
if (len === 2) return true; // ".."
return request.charCodeAt(2) === CHAR_SLASH; // "../..."
const c2 = request.charCodeAt(2);
return c2 === CHAR_SLASH || c2 === CHAR_BACKSLASH; // "../..." or "..\\..."
};

/**
Expand Down
74 changes: 74 additions & 0 deletions test/path.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use strict";

const path = require("path");

const {
PathType,
createCachedBasename,
Expand Down Expand Up @@ -27,6 +29,10 @@ describe("util/path getType", () => {
expect(getType("a")).toBe(PathType.Normal);
});

it("classifies win32 relative two-character inputs", () => {
expect(getType(".\\")).toBe(PathType.Relative);
});

it("classifies two-character inputs", () => {
expect(getType("..")).toBe(PathType.Relative);
expect(getType("./")).toBe(PathType.Relative);
Expand All @@ -39,6 +45,12 @@ describe("util/path getType", () => {
expect(getType("1:")).toBe(PathType.Normal);
});

it("classifies win32 relative longer inputs", () => {
expect(getType(".\\a")).toBe(PathType.Relative);
expect(getType("..\\a")).toBe(PathType.Relative);
expect(getType("..\\..\\src")).toBe(PathType.Relative);
});

it("classifies longer inputs", () => {
expect(getType("./a")).toBe(PathType.Relative);
expect(getType("../a")).toBe(PathType.Relative);
Expand Down Expand Up @@ -99,6 +111,12 @@ describe("util/path normalize", () => {
expect(normalize("./a/../b")).toBe("./b");
});

it("normalizes win32 relative paths to posix", () => {
expect(normalize(".\\a\\b")).toBe("./a/b");
expect(normalize("..\\a")).toBe("../a");
expect(normalize("..\\..\\src\\shared")).toBe("../../src/shared");
});

it("normalizes posix absolute paths", () => {
expect(normalize("/a/b/../c")).toBe("/a/c");
});
Expand Down Expand Up @@ -140,6 +158,11 @@ describe("util/path join", () => {
expect(join("C:\\a", "b")).toBe("C:\\a\\b");
});

it("joins win32 relative requests onto win32 roots", () => {
expect(join("C:\\project\\app", "..\\..\\src")).toBe("C:\\src");
expect(join("C:\\project", ".\\src")).toBe("C:\\project\\src");
});

it("joins DOS device paths with win32 semantics", () => {
expect(join("\\\\?\\C:\\a", "b")).toBe("\\\\?\\C:\\a\\b");
expect(join("\\\\.\\C:\\a", "b")).toBe("\\\\.\\C:\\a\\b");
Expand Down Expand Up @@ -254,6 +277,14 @@ describe("util/path isRelativeRequest", () => {
expect(isRelativeRequest("../foo")).toBe(true);
});

it("returns true for win32 relative requests", () => {
expect(isRelativeRequest(".\\")).toBe(true);
expect(isRelativeRequest(".\\foo")).toBe(true);
expect(isRelativeRequest("..\\")).toBe(true);
expect(isRelativeRequest("..\\foo")).toBe(true);
expect(isRelativeRequest("..\\..\\src")).toBe(true);
});

it("returns false for bare specifiers and absolute paths", () => {
expect(isRelativeRequest("")).toBe(false);
expect(isRelativeRequest("foo")).toBe(false);
Expand Down Expand Up @@ -352,3 +383,46 @@ describe("util/path join fallbacks for special rootPath types", () => {
expect(join("#x", "./foo")).toBe("./#x");
});
});

describe("util/path win32 relative paths from path.relative()", () => {
const isWin32 = process.platform === "win32";

if (isWin32) {
it("should produce backslash paths from path.relative() on win32", () => {
const rel = path.relative("C:\\project\\app", "C:\\project\\src");
expect(rel).toContain("\\");
expect(getType(rel)).toBe(PathType.Relative);
expect(isRelativeRequest(rel)).toBe(true);
expect(normalize(rel)).toBe("../src");
});

it("should handle multi-level win32 relative paths", () => {
const rel = path.relative(
"C:\\project\\packages\\app",
"C:\\project\\src\\shared",
);
expect(rel).toContain("\\");
expect(getType(rel)).toBe(PathType.Relative);
expect(normalize(rel)).toBe("../../src/shared");
});
} else {
it("should handle win32-style paths on any platform", () => {
// Use path.win32.relative() to simulate Windows behavior on non-Windows
const rel = path.win32.relative("C:\\project\\app", "C:\\project\\src");
expect(rel).toBe("..\\src");
expect(getType(rel)).toBe(PathType.Relative);
expect(isRelativeRequest(rel)).toBe(true);
expect(normalize(rel)).toBe("../src");
});

it("should handle multi-level win32 relative paths on any platform", () => {
const rel = path.win32.relative(
"C:\\project\\packages\\app",
"C:\\project\\src\\shared",
);
expect(rel).toBe("..\\..\\src\\shared");
expect(getType(rel)).toBe(PathType.Relative);
expect(normalize(rel)).toBe("../../src/shared");
});
}
});
60 changes: 60 additions & 0 deletions test/win32-relative.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"use strict";

const path = require("path");
const resolve = require("../");

const fixtures = path.join(__dirname, "fixtures");

describe("win32 relative path resolution", () => {
it("should resolve a win32 relative request with backslashes", (done) => {
// Simulate the issue: on Windows, path.relative() returns "..\b.js"
// instead of "../b.js". The resolver should handle both.
const context = path.join(fixtures, "extensions");
resolve(context, "..\\b.js", (err, result) => {
if (err) return done(err);
expect(result).toBe(path.join(fixtures, "b.js"));
done();
});
});

it("should resolve a win32 relative request with backslashes (sync)", () => {
const context = path.join(fixtures, "extensions");
const result = resolve.sync(context, "..\\b.js");
expect(result).toBe(path.join(fixtures, "b.js"));
});

it("should resolve multi-level win32 relative paths", (done) => {
const context = path.join(fixtures, "extensions", "foo");
resolve(context, "..\\..\\a.js", (err, result) => {
if (err) return done(err);
expect(result).toBe(path.join(fixtures, "a.js"));
done();
});
});

it("should resolve win32 relative directory requests", (done) => {
const context = path.join(fixtures, "extensions");
resolve(context, ".\\foo", (err, result) => {
if (err) return done(err);
expect(result).toBe(path.join(fixtures, "extensions", "foo.js"));
done();
});
});

if (process.platform === "win32") {
it("should resolve real path.relative() output on Windows", (done) => {
const from = path.join(fixtures, "extensions");
const to = path.join(fixtures, "b.js");
const rel = path.relative(from, to);

// On Windows, path.relative() produces backslash paths
expect(rel).toContain("\\");

resolve(from, rel, (err, result) => {
if (err) return done(err);
expect(result).toBe(to);
done();
});
});
}
});