Skip to content

Commit 9a8de57

Browse files
mcollinaclaude
andcommitted
vfs: fix Windows path handling and remove wrapper functions
Use path.resolve() instead of pathPosix.normalize() for VFS path normalization so mount points resolve correctly on Windows (e.g. /virtual -> C:\virtual). Use path.sep and path.relative() in router for cross-platform mount point matching. Remove normalizeVFSPath and joinVFSPath wrappers in favor of direct path utility calls. Update tests to use path.resolve()/path.normalize() for platform-portable assertions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a481f68 commit 9a8de57

File tree

11 files changed

+147
-156
lines changed

11 files changed

+147
-156
lines changed

lib/internal/vfs/file_system.js

Lines changed: 10 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -14,44 +14,7 @@ const {
1414
const { validateBoolean } = require('internal/validators');
1515
const { MemoryProvider } = require('internal/vfs/providers/memory');
1616
const path = require('path');
17-
const pathPosix = path.posix;
18-
const { isAbsolute, resolve: resolvePath } = path;
19-
20-
/**
21-
* Normalizes a VFS path. Uses POSIX normalization for all paths except
22-
* Windows drive-letter paths (e.g. C:\), which use platform normalization.
23-
*
24-
* On Windows, a bare '/' prefix is treated as a POSIX VFS path, not as the
25-
* current-drive root.
26-
* @param {string} inputPath The path to normalize
27-
* @returns {string} The normalized path
28-
*/
29-
function normalizeVFSPath(inputPath) {
30-
if (inputPath.length >= 3 && inputPath[1] === ':') {
31-
// Windows drive-letter path (e.g. C:\app)
32-
return path.normalize(inputPath);
33-
}
34-
return pathPosix.normalize(inputPath);
35-
}
36-
37-
/**
38-
* Joins VFS paths. Uses POSIX join for Unix-style base paths and platform
39-
* join for Windows drive-letter paths (e.g. C:\).
40-
*
41-
* On Windows, a bare '/' prefix is treated as a POSIX VFS path, not as the
42-
* current-drive root. Only explicit drive-letter paths like 'C:\app' use
43-
* platform path.join.
44-
* @param {string} base The base path
45-
* @param {string} part The path part to join
46-
* @returns {string} The joined path
47-
*/
48-
function joinVFSPath(base, part) {
49-
if (base.length >= 3 && base[1] === ':') {
50-
// Windows drive-letter path (e.g. C:\app)
51-
return path.join(base, part);
52-
}
53-
return pathPosix.join(base, part);
54-
}
17+
const { isAbsolute, resolve: resolvePath, join: joinPath } = path;
5518
const {
5619
isUnderMountPoint,
5720
getRelativePath,
@@ -238,13 +201,13 @@ class VirtualFileSystem {
238201
resolvePath(inputPath) {
239202
// If path is absolute, return as-is
240203
if (isAbsolute(inputPath)) {
241-
return normalizeVFSPath(inputPath);
204+
return resolvePath(inputPath);
242205
}
243206

244207
// If virtual cwd is enabled and set, resolve relative to it
245208
if (this[kVirtualCwdEnabled] && this[kVirtualCwd] !== null) {
246209
const resolved = `${this[kVirtualCwd]}/${inputPath}`;
247-
return normalizeVFSPath(resolved);
210+
return resolvePath(resolved);
248211
}
249212

250213
// Fall back to resolving the path (will use real cwd)
@@ -262,7 +225,7 @@ class VirtualFileSystem {
262225
if (this[kMounted]) {
263226
throw new ERR_INVALID_STATE('VFS is already mounted');
264227
}
265-
this[kMountPoint] = normalizeVFSPath(prefix);
228+
this[kMountPoint] = resolvePath(prefix);
266229
this[kMounted] = true;
267230
if (this[kModuleHooks]) {
268231
loadModuleHooks();
@@ -331,7 +294,7 @@ class VirtualFileSystem {
331294
process.chdir = function chdir(directory) {
332295
// Normalize path for VFS comparison (preserves forward slashes for Unix-style paths)
333296
const normalized = isAbsolute(directory) ?
334-
normalizeVFSPath(directory) :
297+
resolvePath(directory) :
335298
resolvePath(directory);
336299

337300
if (vfs.shouldHandle(normalized)) {
@@ -374,16 +337,16 @@ class VirtualFileSystem {
374337
* @returns {string} The provider-relative path
375338
*/
376339
#toProviderPath(inputPath) {
377-
const resolved = this.resolvePath(inputPath);
378-
379340
if (this[kMounted] && this[kMountPoint]) {
341+
const resolved = this.resolvePath(inputPath);
380342
if (!isUnderMountPoint(resolved, this[kMountPoint])) {
381343
throw createENOENT('open', inputPath);
382344
}
383345
return getRelativePath(resolved, this[kMountPoint]);
384346
}
385347

386-
return resolved;
348+
// Not mounted: paths are provider-internal, keep POSIX format
349+
return path.posix.normalize(inputPath);
387350
}
388351

389352
/**
@@ -393,7 +356,7 @@ class VirtualFileSystem {
393356
*/
394357
#toMountedPath(providerPath) {
395358
if (this[kMounted] && this[kMountPoint]) {
396-
return joinVFSPath(this[kMountPoint], providerPath);
359+
return joinPath(this[kMountPoint], providerPath);
397360
}
398361
return providerPath;
399362
}
@@ -410,7 +373,7 @@ class VirtualFileSystem {
410373
return false;
411374
}
412375

413-
const normalized = normalizeVFSPath(inputPath);
376+
const normalized = resolvePath(inputPath);
414377
if (!isUnderMountPoint(normalized, this[kMountPoint])) {
415378
return false;
416379
}

lib/internal/vfs/module_hooks.js

Lines changed: 31 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,6 @@ const {
1717

1818
const path = require('path');
1919
const { dirname, extname, isAbsolute, resolve } = path;
20-
const pathPosix = path.posix;
21-
22-
/**
23-
* Normalizes a VFS path. Uses POSIX normalization for Unix-style paths (starting with /)
24-
* and platform normalization for Windows drive letter paths.
25-
* @param {string} inputPath The path to normalize
26-
* @returns {string} The normalized path
27-
*/
28-
function normalizeVFSPath(inputPath) {
29-
// If path starts with / (Unix-style), use posix normalization to preserve forward slashes
30-
if (inputPath.startsWith('/')) {
31-
return pathPosix.normalize(inputPath);
32-
}
33-
// Otherwise use platform normalization (for Windows drive letters like C:\)
34-
return path.normalize(inputPath);
35-
}
3620
const { isURL, pathToFileURL, fileURLToPath, toPathIfFileURL, URL } = require('internal/url');
3721
const { getLazy, kEmptyObject } = require('internal/util');
3822
const { validateObject } = require('internal/validators');
@@ -125,7 +109,7 @@ function vfsStat(vfs, filePath) {
125109
* @returns {{ vfs: VirtualFileSystem, result: number }|null}
126110
*/
127111
function findVFSForStat(filename) {
128-
const normalized = normalizeVFSPath(filename);
112+
const normalized = resolve(filename);
129113
for (let i = 0; i < activeVFSList.length; i++) {
130114
const vfs = activeVFSList[i];
131115
if (vfs.shouldHandle(normalized)) {
@@ -147,7 +131,7 @@ function findVFSForStat(filename) {
147131
* @returns {{ vfs: VirtualFileSystem, content: Buffer|string }|null}
148132
*/
149133
function findVFSForRead(filename, options) {
150-
const normalized = normalizeVFSPath(filename);
134+
const normalized = resolve(filename);
151135
for (let i = 0; i < activeVFSList.length; i++) {
152136
const vfs = activeVFSList[i];
153137
if (vfs.shouldHandle(normalized)) {
@@ -186,7 +170,7 @@ function findVFSForRead(filename, options) {
186170
* @returns {{ vfs: VirtualFileSystem, exists: boolean }|null}
187171
*/
188172
function findVFSForExists(filename) {
189-
const normalized = normalizeVFSPath(filename);
173+
const normalized = resolve(filename);
190174
for (let i = 0; i < activeVFSList.length; i++) {
191175
const vfs = activeVFSList[i];
192176
if (vfs.shouldHandle(normalized)) {
@@ -207,7 +191,7 @@ function findVFSForExists(filename) {
207191
* @returns {{ vfs: VirtualFileSystem, realpath: string }|null}
208192
*/
209193
function findVFSForRealpath(filename) {
210-
const normalized = normalizeVFSPath(filename);
194+
const normalized = resolve(filename);
211195
for (let i = 0; i < activeVFSList.length; i++) {
212196
const vfs = activeVFSList[i];
213197
if (vfs.shouldHandle(normalized)) {
@@ -234,7 +218,7 @@ function findVFSForRealpath(filename) {
234218
* @returns {{ vfs: VirtualFileSystem, stats: Stats }|null}
235219
*/
236220
function findVFSForFsStat(filename) {
237-
const normalized = normalizeVFSPath(filename);
221+
const normalized = resolve(filename);
238222
for (let i = 0; i < activeVFSList.length; i++) {
239223
const vfs = activeVFSList[i];
240224
if (vfs.shouldHandle(normalized)) {
@@ -262,7 +246,7 @@ function findVFSForFsStat(filename) {
262246
* @returns {{ vfs: VirtualFileSystem, entries: string[]|Dirent[] }|null}
263247
*/
264248
function findVFSForReaddir(dirname, options) {
265-
const normalized = normalizeVFSPath(dirname);
249+
const normalized = resolve(dirname);
266250
for (let i = 0; i < activeVFSList.length; i++) {
267251
const vfs = activeVFSList[i];
268252
if (vfs.shouldHandle(normalized)) {
@@ -290,7 +274,7 @@ function findVFSForReaddir(dirname, options) {
290274
* @returns {Promise<{ vfs: VirtualFileSystem, entries: string[]|Dirent[] }|null>}
291275
*/
292276
async function findVFSForReaddirAsync(dirname, options) {
293-
const normalized = normalizeVFSPath(dirname);
277+
const normalized = resolve(dirname);
294278
for (let i = 0; i < activeVFSList.length; i++) {
295279
const vfs = activeVFSList[i];
296280
if (vfs.shouldHandle(normalized)) {
@@ -317,7 +301,7 @@ async function findVFSForReaddirAsync(dirname, options) {
317301
* @returns {Promise<{ vfs: VirtualFileSystem, stats: Stats }|null>}
318302
*/
319303
async function findVFSForLstatAsync(filename) {
320-
const normalized = normalizeVFSPath(filename);
304+
const normalized = resolve(filename);
321305
for (let i = 0; i < activeVFSList.length; i++) {
322306
const vfs = activeVFSList[i];
323307
if (vfs.shouldHandle(normalized)) {
@@ -345,7 +329,7 @@ async function findVFSForLstatAsync(filename) {
345329
* @returns {{ vfs: VirtualFileSystem }|null}
346330
*/
347331
function findVFSForWatch(filename) {
348-
const normalized = normalizeVFSPath(filename);
332+
const normalized = resolve(filename);
349333
for (let i = 0; i < activeVFSList.length; i++) {
350334
const vfs = activeVFSList[i];
351335
if (vfs.shouldHandle(normalized)) {
@@ -394,7 +378,7 @@ function getVFSPackageType(vfs, filePath) {
394378
StringPrototypeEndsWith(currentDir, '\\node_modules')) {
395379
break;
396380
}
397-
const pjsonPath = normalizeVFSPath(resolve(currentDir, 'package.json'));
381+
const pjsonPath = resolve(currentDir, 'package.json');
398382
if (vfs.shouldHandle(pjsonPath) && vfsStat(vfs, pjsonPath) === 0) {
399383
try {
400384
const content = vfs.readFileSync(pjsonPath, 'utf8');
@@ -454,7 +438,7 @@ function tryExtensions(vfs, basePath) {
454438
function tryIndexFiles(vfs, dirPath) {
455439
const indexFiles = ['index.js', 'index.mjs', 'index.cjs', 'index.json'];
456440
for (let i = 0; i < indexFiles.length; i++) {
457-
const candidate = normalizeVFSPath(resolve(dirPath, indexFiles[i]));
441+
const candidate = resolve(dirPath, indexFiles[i]);
458442
if (vfsStat(vfs, candidate) === 0) {
459443
const url = pathToFileURL(candidate).href;
460444
const format = getVFSFormat(vfs, candidate);
@@ -479,7 +463,7 @@ function resolveConditions(vfs, pkgDir, condMap, conditions) {
479463
if (key === 'default' || ArrayPrototypeIndexOf(conditions, key) !== -1) {
480464
const value = condMap[key];
481465
if (typeof value === 'string') {
482-
const resolved = normalizeVFSPath(resolve(pkgDir, value));
466+
const resolved = resolve(pkgDir, value);
483467
if (vfsStat(vfs, resolved) === 0) {
484468
const url = pathToFileURL(resolved).href;
485469
const format = getVFSFormat(vfs, resolved);
@@ -514,7 +498,7 @@ function resolvePackageExports(vfs, pkgDir, packageSubpath, exports, context) {
514498
// String shorthand: "exports": "./index.js"
515499
if (typeof exports === 'string') {
516500
if (packageSubpath === '.') {
517-
const resolved = normalizeVFSPath(resolve(pkgDir, exports));
501+
const resolved = resolve(pkgDir, exports);
518502
if (vfsStat(vfs, resolved) === 0) {
519503
const url = pathToFileURL(resolved).href;
520504
const format = getVFSFormat(vfs, resolved);
@@ -544,7 +528,7 @@ function resolvePackageExports(vfs, pkgDir, packageSubpath, exports, context) {
544528
if (target === undefined) return null;
545529

546530
if (typeof target === 'string') {
547-
const resolved = normalizeVFSPath(resolve(pkgDir, target));
531+
const resolved = resolve(pkgDir, target);
548532
if (vfsStat(vfs, resolved) === 0) {
549533
const url = pathToFileURL(resolved).href;
550534
const format = getVFSFormat(vfs, resolved);
@@ -569,7 +553,7 @@ function resolvePackageExports(vfs, pkgDir, packageSubpath, exports, context) {
569553
* @returns {object|null} Resolve result or null
570554
*/
571555
function resolveDirectoryEntry(vfs, dirPath, context) {
572-
const pjsonPath = normalizeVFSPath(resolve(dirPath, 'package.json'));
556+
const pjsonPath = resolve(dirPath, 'package.json');
573557
if (vfsStat(vfs, pjsonPath) === 0) {
574558
try {
575559
const content = vfs.readFileSync(pjsonPath, 'utf8');
@@ -584,7 +568,7 @@ function resolveDirectoryEntry(vfs, dirPath, context) {
584568

585569
// Try main
586570
if (parsed.main) {
587-
const mainPath = normalizeVFSPath(resolve(dirPath, parsed.main));
571+
const mainPath = resolve(dirPath, parsed.main);
588572
if (vfsStat(vfs, mainPath) === 0) {
589573
const url = pathToFileURL(mainPath).href;
590574
const format = getVFSFormat(vfs, mainPath);
@@ -635,7 +619,7 @@ function parsePackageName(specifier) {
635619
* @returns {object} Resolve result
636620
*/
637621
function resolveVFSPath(checkPath, context, nextResolve, specifier) {
638-
const normalized = normalizeVFSPath(checkPath);
622+
const normalized = resolve(checkPath);
639623

640624
for (let i = 0; i < activeVFSList.length; i++) {
641625
const vfs = activeVFSList[i];
@@ -695,7 +679,7 @@ function resolveBareSpecifier(specifier, context, nextResolve) {
695679
}
696680

697681
// Find which VFS handles the parent
698-
const parentNorm = normalizeVFSPath(parentPath);
682+
const parentNorm = resolve(parentPath);
699683
let parentVfs = null;
700684
for (let i = 0; i < activeVFSList.length; i++) {
701685
if (activeVFSList[i].shouldHandle(parentNorm)) {
@@ -715,13 +699,12 @@ function resolveBareSpecifier(specifier, context, nextResolve) {
715699
let lastDir;
716700

717701
while (currentDir !== lastDir) {
718-
const pkgDir = normalizeVFSPath(
719-
resolve(currentDir, 'node_modules', packageName));
702+
const pkgDir = resolve(currentDir, 'node_modules', packageName);
720703

721704
if (parentVfs.shouldHandle(pkgDir) &&
722705
vfsStat(parentVfs, pkgDir) === 1) {
723706
// Found the package directory
724-
const pjsonPath = normalizeVFSPath(resolve(pkgDir, 'package.json'));
707+
const pjsonPath = resolve(pkgDir, 'package.json');
725708
if (vfsStat(parentVfs, pjsonPath) === 0) {
726709
try {
727710
const content = parentVfs.readFileSync(pjsonPath, 'utf8');
@@ -737,7 +720,7 @@ function resolveBareSpecifier(specifier, context, nextResolve) {
737720
// No exports, resolve subpath directly
738721
if (packageSubpath === '.') {
739722
if (parsed.main) {
740-
const mainPath = normalizeVFSPath(resolve(pkgDir, parsed.main));
723+
const mainPath = resolve(pkgDir, parsed.main);
741724
if (vfsStat(parentVfs, mainPath) === 0) {
742725
const url = pathToFileURL(mainPath).href;
743726
const format = getVFSFormat(parentVfs, mainPath);
@@ -754,8 +737,7 @@ function resolveBareSpecifier(specifier, context, nextResolve) {
754737
if (indexResult) return indexResult;
755738
} else {
756739
// Resolve subpath like './feature'
757-
const subResolved = normalizeVFSPath(
758-
resolve(pkgDir, packageSubpath));
740+
const subResolved = resolve(pkgDir, packageSubpath);
759741
if (vfsStat(parentVfs, subResolved) === 0) {
760742
const url = pathToFileURL(subResolved).href;
761743
const format = getVFSFormat(parentVfs, subResolved);
@@ -812,7 +794,14 @@ function vfsResolveHook(specifier, context, nextResolve) {
812794
// Convert specifier to an absolute path
813795
let checkPath;
814796
if (StringPrototypeStartsWith(specifier, 'file:')) {
815-
checkPath = fileURLToPath(specifier);
797+
try {
798+
checkPath = fileURLToPath(specifier);
799+
} catch {
800+
// On Windows, file: URLs without a drive letter (e.g. file:///mh1/...)
801+
// are invalid. Extract the path and resolve it with a drive letter.
802+
const url = new URL(specifier);
803+
checkPath = resolve(url.pathname);
804+
}
816805
} else if (isAbsolute(specifier)) {
817806
checkPath = specifier;
818807
} else if (specifier[0] === '.') {
@@ -851,7 +840,7 @@ function vfsLoadHook(url, context, nextLoad) {
851840
}
852841

853842
const filePath = fileURLToPath(url);
854-
const normalized = normalizeVFSPath(filePath);
843+
const normalized = resolve(filePath);
855844

856845
// Check if any VFS handles this path
857846
for (let i = 0; i < activeVFSList.length; i++) {

0 commit comments

Comments
 (0)