🔎 Search Terms
normalizePath, getNormalizedAbsolutePath, simpleNormalizePath, leading ./, relative path rooted
🕗 Version & Regression Information
Regression introduced by #60812 ("Write path normalization without array allocations"), which added the simpleNormalizePath fast path.
⏯ Playground / Reproduction
normalizePath and getNormalizedAbsolutePath turn a relative path that begins with ./ followed by a redundant separator into a rooted path:
import { normalizePath, getNormalizedAbsolutePath } from "typescript";
normalizePath(".//a"); // "/a" — expected "a"
normalizePath("././/a"); // "/a" — expected "a"
getNormalizedAbsolutePath(".//a/b", ""); // "/a/b" — expected "a/b"
🙁 Actual behavior
simpleNormalizePath runs path.replace(/\/\.\//g, "/") (a no-op for .//a) and then if (simplified.startsWith("./")) simplified = simplified.slice(2). For .//a that strips ./ and leaves /a — but the second / was a redundant separator, not a root — so the result is returned as a rooted path. A relative path is silently converted to an absolute one. The slow-path component walker returns the correct relative result.
🙂 Expected behavior
.//a → a, .//a/b → a/b, ././/a → a (stay relative), matching the slow path.
Additional information
Proposed fix in #63587: only strip the leading ./ when the next character is not another separator (otherwise fall through to the slow path). Verified on the bundled compiler with an exhaustive differential over all strings up to length 8 from {'.', '/', 'a'} (9,840 inputs): 172 results change, every one a ./-then-separator case corrected to match the slow path; zero changes elsewhere.
🔎 Search Terms
normalizePath, getNormalizedAbsolutePath, simpleNormalizePath, leading ./, relative path rooted
🕗 Version & Regression Information
Regression introduced by #60812 ("Write path normalization without array allocations"), which added the
simpleNormalizePathfast path.⏯ Playground / Reproduction
normalizePathandgetNormalizedAbsolutePathturn a relative path that begins with./followed by a redundant separator into a rooted path:🙁 Actual behavior
simpleNormalizePathrunspath.replace(/\/\.\//g, "/")(a no-op for.//a) and thenif (simplified.startsWith("./")) simplified = simplified.slice(2). For.//athat strips./and leaves/a— but the second/was a redundant separator, not a root — so the result is returned as a rooted path. A relative path is silently converted to an absolute one. The slow-path component walker returns the correct relative result.🙂 Expected behavior
.//a→a,.//a/b→a/b,././/a→a(stay relative), matching the slow path.Additional information
Proposed fix in #63587: only strip the leading
./when the next character is not another separator (otherwise fall through to the slow path). Verified on the bundled compiler with an exhaustive differential over all strings up to length 8 from{'.', '/', 'a'}(9,840 inputs): 172 results change, every one a./-then-separator case corrected to match the slow path; zero changes elsewhere.