fix: route UNC paths through win32 instead of posix - #644
Conversation
`getType` only accepted the DOS device forms (`\\?\`, `\\.\`) as
windows-absolute, so a plain share fell through to `Normal` and every helper
answered it with posix semantics: `dirname("\\\\server\\share\\a\\b")` was
`"."`, `join` produced the mixed `\\server\share\a/index.js`, `normalize`
left `.` and `..` segments in place, and `Resolver.isModule` reported a share
path as a bare module request, sending it to `node_modules` instead of
resolving it as a path.
`path.win32` roots any path starting with two backslashes, so both forms are
windows-absolute and both now take that branch. A single leading backslash
stays out of it: on Windows it is a rooted path, everywhere else an ordinary
filename character, and nothing in the string tells the two apart.
🦋 Changeset detectedLatest commit: 905586c The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #644 +/- ##
==========================================
- Coverage 98.12% 98.11% -0.01%
==========================================
Files 49 49
Lines 9902 10010 +108
==========================================
+ Hits 9716 9821 +105
- Misses 186 189 +3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
`path.win32` reads any two leading separators as a UNC root, including the mixed `/\` spelling. A leading forward slash is unambiguously a posix root here, so it keeps posix semantics whatever follows it.
Merging this PR will regress 1 benchmark
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | Simulation | extensions-many: 6-extension list (warm) |
994.2 µs | 1,344.2 µs | -26.04% |
| ⚡ | Memory | node-compare: node require.resolve x 1000 |
29.2 KB | 21.2 KB | +37.49% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing claude/enhanced-resolve-unc-paths (5881591) with main (d8693b6)
The helpers pick a flavor by path shape and then have to answer the way that flavor does. That is now asserted directly rather than case by case: for a curated list of shapes and for 5000 seeded generated ones, the flavor, `normalize`, `dirname` and `join` are compared against `path.win32` or `path.posix`, whichever node reads the path as. Every shape we answer differently on purpose is listed in one place and asserted on its own, together with what node does with it, so a divergence can only be added deliberately: a leading forward slash, a single leading backslash, a drive-relative path, and relative, empty and internal requests.
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## enhanced-resolve@5.24.4 ### Patch Changes - Keep the original request resolvable when `extensionAlias` lists its own extension. (by [@alexander-akait](https://github.com/alexander-akait) in [#641](#641)) - Fix string `restrictions` boundary checks: a restriction ending with a separator no longer rejects everything inside it, restrictions are normalized before they are compared, and a Windows path now matches the way `path.win32` does, treating `/` and `\` as interchangeable and comparing case-insensitively, while `\` stays a filename character in a posix path. The same comparison backs `tsconfig` path matching. (by [@alexander-akait](https://github.com/alexander-akait) in [#643](#643)) - Treat a UNC path (`\\server\share\…`) as a Windows path, so it normalizes, joins and walks up with `path.win32` semantics instead of being taken for a bare module request. (by [@alexander-akait](https://github.com/alexander-akait) in [#644](#644)) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Summary
getTypeaccepted only the DOS device forms (\\?\,\\.\) as windows-absolute, so a plain UNC share fell through toNormaland every helper answered it with posix semantics.path.win32roots any path starting with two backslashes, so both forms are windows-absolute and both now take that branch.path.win32dirname("\\\\server\\share\\a\\b")"."\\server\share\ajoin("\\\\server\\share\\a", "./index.js")\\server\share\a/index.js\\server\share\a\index.jsnormalize("\\\\server\\share\\a\\..\\b")\\server\share\bResolver.isModule("\\\\server\\share\\a")truefalseThe
dirnameandisModulerows are the ones that bite: walking up from a share to find apackage.jsongave up immediately at".", and an absolute share path was reported as a bare module request, so it was looked for innode_modulesinstead of being resolved as a path. Resolving anything on a network share was broken as a result.getDosDeviceTypeis gone since the two forms no longer need telling apart, which also drops a call fromgetType; it measures marginally faster (89 ms vs 94 ms per 11M calls on a realistic mix of requests).The helpers pick a flavor by path shape and then have to answer the way that flavor does, so
test/path.test.jsnow asserts that directly againstpath.win32/path.posix— a curated list of shapes plus 5000 seeded generated ones, comparing the flavor,normalize,dirnameandjoin(~270 ms). Reverting this fix fails 22 of them.Every shape we answer differently on purpose is listed in one place and asserted on its own, next to what node does with it, so a divergence can only be added deliberately:
path.win32reads//server/shareand/\server\shareas UNC roots.C:foo, no separator after the colon) stayNormal, as they already did onmain.path.win32gives them the rootC:but reports them as not absolute, andPathTypehas no windows-relative member — classifying them asAbsoluteWinwould makejointreat them as absolute and drop the root, trading one misalignment for another. Worth its own change.#…) requests, where the resolver deliberately keeps its own semantics (a./prefix is preserved, an empty path stays empty).Found while reviewing #643, which fixes the restriction boundary check. That PR is independent of this one — its comparison already handles UNC — and they touch different parts of
lib/util/path.js.What kind of change does this PR introduce?
fix
Did you add tests for your changes?
Yes.
test/unc-paths.test.jsresolves against a UNC context through the public API — a relative request, a nested directory, an absolute share request from an unrelated context, and a description-filemain— all four fail onmainand pass here. They run on every host against a fake filesystem, since a real share cannot be mounted in CI and the resolver picks the path flavor from the path rather than from the host.test/path.test.jscoversgetType,normalize,joinanddirnamedirectly, and adds the alignment suite described above.Does this PR introduce a breaking change?
No, unless a posix path literally starting with
\\was being resolved, which would previously have been treated as a relative filename.If relevant, what needs to be documented once your changes are merged?
n/a
Use of AI
Written with Claude Code. It compared every helper against
path.win32/path.posixbefore and after — a curated matrix plus 70k generated shapes — wrote the failing tests first, mutation-tested the new alignment suite to confirm it catches the regression, and benchmarkedgetTypeto confirm the hot path did not regress; I reviewed the result.npm run lint,npm run test:onlyandnpm run test:browserall pass locally.