Skip to content

Commit 3b941d1

Browse files
committed
vfs: add Windows path compatibility
- Use path.isAbsolute() for cross-platform absolute path detection instead of checking for '/' prefix only - Use path.posix.join() in createScopedVFS to ensure forward slashes on all platforms since VFS uses '/' internally - Fix ESM module hook to recognize Windows absolute paths (C:\) - Fix symlink target resolution for Windows paths This enables VFS to work correctly on Windows where absolute paths use drive letters (e.g., C:\path) instead of Unix-style forward slashes.
1 parent 977cc3d commit 3b941d1

File tree

5 files changed

+34
-26
lines changed

5 files changed

+34
-26
lines changed

lib/internal/vfs/entries.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ const {
1212
ERR_INVALID_STATE,
1313
},
1414
} = require('internal/errors');
15-
const { join } = require('path');
15+
// Use path.posix.join for cross-platform consistency - VFS uses forward slashes internally
16+
const { posix: { join } } = require('path');
1617
const { createFileStats, createDirectoryStats, createSymlinkStats } = require('internal/vfs/stats');
1718

1819
// Symbols for private properties

lib/internal/vfs/module_hooks.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const {
88
StringPrototypeStartsWith,
99
} = primordials;
1010

11-
const { dirname, resolve } = require('path');
11+
const { dirname, isAbsolute, resolve } = require('path');
1212
const { normalizePath } = require('internal/vfs/router');
1313
const { pathToFileURL, fileURLToPath, URL } = require('internal/url');
1414
const { createENOENT } = require('internal/vfs/errors');
@@ -331,8 +331,8 @@ function vfsResolveHook(specifier, context, nextResolve) {
331331
let checkPath;
332332
if (StringPrototypeStartsWith(specifier, 'file:')) {
333333
checkPath = fileURLToPath(specifier);
334-
} else if (specifier[0] === '/') {
335-
// Absolute path
334+
} else if (isAbsolute(specifier)) {
335+
// Absolute path (Unix / or Windows C:\)
336336
checkPath = specifier;
337337
} else if (specifier[0] === '.') {
338338
// Relative path - need to resolve against parent

lib/internal/vfs/router.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const {
99
StringPrototypeStartsWith,
1010
} = primordials;
1111

12-
const { basename, resolve, sep } = require('path');
12+
const { basename, isAbsolute, resolve, sep } = require('path');
1313

1414
/**
1515
* Normalizes a path for VFS lookup.
@@ -131,4 +131,5 @@ module.exports = {
131131
isUnderMountPoint,
132132
getRelativePath,
133133
joinMountPath,
134+
isAbsolutePath: isAbsolute,
134135
};

lib/internal/vfs/virtual_fs.js

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const {
2525
getBaseName,
2626
isUnderMountPoint,
2727
getRelativePath,
28+
isAbsolutePath,
2829
} = require('internal/vfs/router');
2930
const {
3031
createENOENT,
@@ -182,8 +183,8 @@ class VirtualFileSystem {
182183
* @returns {string} The resolved path
183184
*/
184185
resolvePath(inputPath) {
185-
// If path is absolute, return as-is
186-
if (inputPath.startsWith('/')) {
186+
// If path is absolute, return as-is (works for both Unix / and Windows C:\)
187+
if (isAbsolutePath(inputPath)) {
187188
return normalizePath(inputPath);
188189
}
189190

@@ -471,13 +472,21 @@ class VirtualFileSystem {
471472
const target = symlink.target;
472473
let targetPath;
473474

474-
if (target.startsWith('/')) {
475+
if (isAbsolutePath(target)) {
475476
// Absolute symlink target - interpret as VFS-internal path
476477
// If mounted, prepend the mount point to get the actual filesystem path
478+
// Normalize first to handle Windows paths (convert \ to /)
479+
const normalizedTarget = normalizePath(target);
477480
if (this[kMounted] && this[kMountPoint]) {
478-
targetPath = this[kMountPoint] + target;
481+
// For VFS-internal absolute paths starting with /, prepend mount point
482+
// For external absolute paths (like C:/...), use as-is
483+
if (target.startsWith('/')) {
484+
targetPath = this[kMountPoint] + normalizedTarget;
485+
} else {
486+
targetPath = normalizedTarget;
487+
}
479488
} else {
480-
targetPath = target;
489+
targetPath = normalizedTarget;
481490
}
482491
} else {
483492
// Relative symlink target - resolve relative to symlink's parent directory
@@ -527,7 +536,6 @@ class VirtualFileSystem {
527536

528537
for (let i = 0; i < segments.length; i++) {
529538
const segment = segments[i];
530-
const isLastSegment = i === segments.length - 1;
531539

532540
// Follow symlinks for intermediate path components
533541
if (current.isSymbolicLink() && followSymlinks) {
@@ -571,7 +579,7 @@ class VirtualFileSystem {
571579
* Resolves a path to its VFS entry, if it exists.
572580
* Follows symlinks by default.
573581
* @param {string} inputPath The path to resolve
574-
* @param {boolean} [followSymlinks=true] Whether to follow symlinks
582+
* @param {boolean} [followSymlinks] Whether to follow symlinks
575583
* @returns {VirtualEntry|null}
576584
* @private
577585
*/
@@ -584,7 +592,7 @@ class VirtualFileSystem {
584592
* Resolves a path and throws ELOOP if symlink loop detected.
585593
* @param {string} inputPath The path to resolve
586594
* @param {string} syscall The syscall name for error
587-
* @param {boolean} [followSymlinks=true] Whether to follow symlinks
595+
* @param {boolean} [followSymlinks] Whether to follow symlinks
588596
* @returns {VirtualEntry}
589597
* @throws {Error} ENOENT if path doesn't exist, ELOOP if symlink loop
590598
* @private

test/parallel/test-vfs-symlinks.js

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict';
22

3-
require('../common');
3+
const common = require('../common');
44
const assert = require('assert');
55
const fs = require('fs');
66

@@ -60,7 +60,7 @@ const fs = require('fs');
6060

6161
const lstat = vfs.lstatSync('/virtual/link.txt');
6262

63-
// lstat should show it's a symlink
63+
// Lstat should show it's a symlink
6464
assert.strictEqual(lstat.isSymbolicLink(), true);
6565
assert.strictEqual(lstat.isFile(), false);
6666

@@ -126,7 +126,7 @@ const fs = require('fs');
126126
const content = vfs.readFileSync('/virtual/dir/link.txt', 'utf8');
127127
assert.strictEqual(content, 'content');
128128

129-
// readlink should return the relative target as-is
129+
// Readlink should return the relative target as-is
130130
const target = vfs.readlinkSync('/virtual/dir/link.txt');
131131
assert.strictEqual(target, 'file.txt');
132132

@@ -229,11 +229,10 @@ const fs = require('fs');
229229
vfs.addSymlink('/link', '/target');
230230
vfs.mount('/virtual');
231231

232-
vfs.readlink('/virtual/link', (err, target) => {
233-
assert.ifError(err);
232+
vfs.readlink('/virtual/link', common.mustSucceed((target) => {
234233
assert.strictEqual(target, '/target');
235234
vfs.unmount();
236-
});
235+
}));
237236
}
238237

239238
// Test async realpath with symlinks
@@ -243,11 +242,10 @@ const fs = require('fs');
243242
vfs.addSymlink('/link', '/real');
244243
vfs.mount('/virtual');
245244

246-
vfs.realpath('/virtual/link/file.txt', (err, resolvedPath) => {
247-
assert.ifError(err);
245+
vfs.realpath('/virtual/link/file.txt', common.mustSucceed((resolvedPath) => {
248246
assert.strictEqual(resolvedPath, '/virtual/real/file.txt');
249247
vfs.unmount();
250-
});
248+
}));
251249
}
252250

253251
// Test promises API - stat follows symlinks
@@ -262,7 +260,7 @@ const fs = require('fs');
262260
assert.strictEqual(stat.isFile(), true);
263261
assert.strictEqual(stat.size, 50);
264262
vfs.unmount();
265-
})();
263+
})().then(common.mustCall());
266264
}
267265

268266
// Test promises API - lstat does not follow symlinks
@@ -276,7 +274,7 @@ const fs = require('fs');
276274
const lstat = await vfs.promises.lstat('/virtual/link.txt');
277275
assert.strictEqual(lstat.isSymbolicLink(), true);
278276
vfs.unmount();
279-
})();
277+
})().then(common.mustCall());
280278
}
281279

282280
// Test promises API - readlink
@@ -290,7 +288,7 @@ const fs = require('fs');
290288
const target = await vfs.promises.readlink('/virtual/link');
291289
assert.strictEqual(target, '/target');
292290
vfs.unmount();
293-
})();
291+
})().then(common.mustCall());
294292
}
295293

296294
// Test promises API - realpath resolves symlinks
@@ -304,7 +302,7 @@ const fs = require('fs');
304302
const resolved = await vfs.promises.realpath('/virtual/link/file.txt');
305303
assert.strictEqual(resolved, '/virtual/real/file.txt');
306304
vfs.unmount();
307-
})();
305+
})().then(common.mustCall());
308306
}
309307

310308
// Test broken symlink (target doesn't exist)

0 commit comments

Comments
 (0)