Skip to content

Commit a481f68

Browse files
committed
vfs: address review feedback on path utilities and providers
- Remove redundant #getBaseName/#getParentPath from MemoryProvider, use pathPosix.basename/dirname directly - Remove redundant getBaseName/getParentPath/splitPath from router.js, keep only functions with non-trivial VFS-specific logic - Convert RealFSProvider constant getters (readonly, supportsSymlinks) to readonly properties via ObjectDefineProperty - Fix joinVFSPath/normalizeVFSPath to detect Windows drive-letter paths by checking for ':' at position 1 instead of checking for leading '/', so bare '/' is always treated as a POSIX VFS path - Update test-vfs-internals.js to match router.js export changes
1 parent 632979a commit a481f68

File tree

7 files changed

+67
-159
lines changed

7 files changed

+67
-159
lines changed

lib/internal/vfs/file_system.js

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,31 +18,39 @@ const pathPosix = path.posix;
1818
const { isAbsolute, resolve: resolvePath } = path;
1919

2020
/**
21-
* Normalizes a VFS path. Uses POSIX normalization for Unix-style paths (starting with /)
22-
* and platform normalization for Windows drive letter paths.
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.
2326
* @param {string} inputPath The path to normalize
2427
* @returns {string} The normalized path
2528
*/
2629
function normalizeVFSPath(inputPath) {
27-
// If path starts with / (Unix-style), use posix normalization to preserve forward slashes
28-
if (inputPath.startsWith('/')) {
29-
return pathPosix.normalize(inputPath);
30+
if (inputPath.length >= 3 && inputPath[1] === ':') {
31+
// Windows drive-letter path (e.g. C:\app)
32+
return path.normalize(inputPath);
3033
}
31-
// Otherwise use platform normalization (for Windows drive letters like C:\)
32-
return path.normalize(inputPath);
34+
return pathPosix.normalize(inputPath);
3335
}
3436

3537
/**
36-
* Joins VFS paths. Uses POSIX join for Unix-style base paths.
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.
3744
* @param {string} base The base path
3845
* @param {string} part The path part to join
3946
* @returns {string} The joined path
4047
*/
4148
function joinVFSPath(base, part) {
42-
if (base.startsWith('/')) {
43-
return pathPosix.join(base, part);
49+
if (base.length >= 3 && base[1] === ':') {
50+
// Windows drive-letter path (e.g. C:\app)
51+
return path.join(base, part);
4452
}
45-
return path.join(base, part);
53+
return pathPosix.join(base, part);
4654
}
4755
const {
4856
isUnderMountPoint,
@@ -602,23 +610,6 @@ class VirtualFileSystem {
602610
this[kProvider].accessSync(providerPath, mode);
603611
}
604612

605-
/**
606-
* Returns the stat result code for module resolution.
607-
* @param {string} filePath The path to check
608-
* @returns {number} 0 for file, 1 for directory, -2 for not found
609-
*/
610-
internalModuleStat(filePath) {
611-
try {
612-
const stats = this.statSync(filePath);
613-
if (stats.isDirectory()) {
614-
return 1;
615-
}
616-
return 0;
617-
} catch {
618-
return -2;
619-
}
620-
}
621-
622613
// ==================== File Descriptor Operations ====================
623614

624615
/**

lib/internal/vfs/module_hooks.js

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,24 @@ function deregisterVFS(vfs) {
101101
// as another VFS might be registered later.
102102
}
103103

104+
/**
105+
* Returns the stat result code for a VFS path.
106+
* @param {VirtualFileSystem} vfs The VFS instance
107+
* @param {string} filePath The path to check
108+
* @returns {number} 0 for file, 1 for directory, -2 for not found
109+
*/
110+
function vfsStat(vfs, filePath) {
111+
try {
112+
const stats = vfs.statSync(filePath);
113+
if (stats.isDirectory()) {
114+
return 1;
115+
}
116+
return 0;
117+
} catch {
118+
return -2;
119+
}
120+
}
121+
104122
/**
105123
* Checks all active VFS instances for a file/directory.
106124
* @param {string} filename The absolute path to check
@@ -111,7 +129,7 @@ function findVFSForStat(filename) {
111129
for (let i = 0; i < activeVFSList.length; i++) {
112130
const vfs = activeVFSList[i];
113131
if (vfs.shouldHandle(normalized)) {
114-
const result = vfs.internalModuleStat(normalized);
132+
const result = vfsStat(vfs, normalized);
115133
// For mounted VFS, always return result (even -2 for ENOENT within mount)
116134
// For overlay VFS, only return if found
117135
if (vfs.mounted || result >= 0) {
@@ -136,7 +154,7 @@ function findVFSForRead(filename, options) {
136154
// Check if the file actually exists in VFS
137155
if (vfs.existsSync(normalized)) {
138156
// Only read files, not directories
139-
const statResult = vfs.internalModuleStat(normalized);
157+
const statResult = vfsStat(vfs, normalized);
140158
if (statResult !== 0) {
141159
// Not a file (1 = dir, -2 = not found)
142160
// Let the real fs handle it (will throw appropriate error)
@@ -377,7 +395,7 @@ function getVFSPackageType(vfs, filePath) {
377395
break;
378396
}
379397
const pjsonPath = normalizeVFSPath(resolve(currentDir, 'package.json'));
380-
if (vfs.shouldHandle(pjsonPath) && vfs.internalModuleStat(pjsonPath) === 0) {
398+
if (vfs.shouldHandle(pjsonPath) && vfsStat(vfs, pjsonPath) === 0) {
381399
try {
382400
const content = vfs.readFileSync(pjsonPath, 'utf8');
383401
const parsed = JSONParse(content);
@@ -420,7 +438,7 @@ function tryExtensions(vfs, basePath) {
420438
const extensions = ['.js', '.json', '.node', '.mjs', '.cjs'];
421439
for (let i = 0; i < extensions.length; i++) {
422440
const candidate = basePath + extensions[i];
423-
if (vfs.internalModuleStat(candidate) === 0) {
441+
if (vfsStat(vfs, candidate) === 0) {
424442
return candidate;
425443
}
426444
}
@@ -437,7 +455,7 @@ function tryIndexFiles(vfs, dirPath) {
437455
const indexFiles = ['index.js', 'index.mjs', 'index.cjs', 'index.json'];
438456
for (let i = 0; i < indexFiles.length; i++) {
439457
const candidate = normalizeVFSPath(resolve(dirPath, indexFiles[i]));
440-
if (vfs.internalModuleStat(candidate) === 0) {
458+
if (vfsStat(vfs, candidate) === 0) {
441459
const url = pathToFileURL(candidate).href;
442460
const format = getVFSFormat(vfs, candidate);
443461
return { url, format, shortCircuit: true };
@@ -462,7 +480,7 @@ function resolveConditions(vfs, pkgDir, condMap, conditions) {
462480
const value = condMap[key];
463481
if (typeof value === 'string') {
464482
const resolved = normalizeVFSPath(resolve(pkgDir, value));
465-
if (vfs.internalModuleStat(resolved) === 0) {
483+
if (vfsStat(vfs, resolved) === 0) {
466484
const url = pathToFileURL(resolved).href;
467485
const format = getVFSFormat(vfs, resolved);
468486
return { url, format, shortCircuit: true };
@@ -497,7 +515,7 @@ function resolvePackageExports(vfs, pkgDir, packageSubpath, exports, context) {
497515
if (typeof exports === 'string') {
498516
if (packageSubpath === '.') {
499517
const resolved = normalizeVFSPath(resolve(pkgDir, exports));
500-
if (vfs.internalModuleStat(resolved) === 0) {
518+
if (vfsStat(vfs, resolved) === 0) {
501519
const url = pathToFileURL(resolved).href;
502520
const format = getVFSFormat(vfs, resolved);
503521
return { url, format, shortCircuit: true };
@@ -527,7 +545,7 @@ function resolvePackageExports(vfs, pkgDir, packageSubpath, exports, context) {
527545

528546
if (typeof target === 'string') {
529547
const resolved = normalizeVFSPath(resolve(pkgDir, target));
530-
if (vfs.internalModuleStat(resolved) === 0) {
548+
if (vfsStat(vfs, resolved) === 0) {
531549
const url = pathToFileURL(resolved).href;
532550
const format = getVFSFormat(vfs, resolved);
533551
return { url, format, shortCircuit: true };
@@ -552,7 +570,7 @@ function resolvePackageExports(vfs, pkgDir, packageSubpath, exports, context) {
552570
*/
553571
function resolveDirectoryEntry(vfs, dirPath, context) {
554572
const pjsonPath = normalizeVFSPath(resolve(dirPath, 'package.json'));
555-
if (vfs.internalModuleStat(pjsonPath) === 0) {
573+
if (vfsStat(vfs, pjsonPath) === 0) {
556574
try {
557575
const content = vfs.readFileSync(pjsonPath, 'utf8');
558576
const parsed = JSONParse(content);
@@ -567,7 +585,7 @@ function resolveDirectoryEntry(vfs, dirPath, context) {
567585
// Try main
568586
if (parsed.main) {
569587
const mainPath = normalizeVFSPath(resolve(dirPath, parsed.main));
570-
if (vfs.internalModuleStat(mainPath) === 0) {
588+
if (vfsStat(vfs, mainPath) === 0) {
571589
const url = pathToFileURL(mainPath).href;
572590
const format = getVFSFormat(vfs, mainPath);
573591
return { url, format, shortCircuit: true };
@@ -623,7 +641,7 @@ function resolveVFSPath(checkPath, context, nextResolve, specifier) {
623641
const vfs = activeVFSList[i];
624642
if (!vfs.shouldHandle(normalized)) continue;
625643

626-
const stat = vfs.internalModuleStat(normalized);
644+
const stat = vfsStat(vfs, normalized);
627645

628646
if (stat === 0) {
629647
// It's a file
@@ -701,10 +719,10 @@ function resolveBareSpecifier(specifier, context, nextResolve) {
701719
resolve(currentDir, 'node_modules', packageName));
702720

703721
if (parentVfs.shouldHandle(pkgDir) &&
704-
parentVfs.internalModuleStat(pkgDir) === 1) {
722+
vfsStat(parentVfs, pkgDir) === 1) {
705723
// Found the package directory
706724
const pjsonPath = normalizeVFSPath(resolve(pkgDir, 'package.json'));
707-
if (parentVfs.internalModuleStat(pjsonPath) === 0) {
725+
if (vfsStat(parentVfs, pjsonPath) === 0) {
708726
try {
709727
const content = parentVfs.readFileSync(pjsonPath, 'utf8');
710728
const parsed = JSONParse(content);
@@ -720,7 +738,7 @@ function resolveBareSpecifier(specifier, context, nextResolve) {
720738
if (packageSubpath === '.') {
721739
if (parsed.main) {
722740
const mainPath = normalizeVFSPath(resolve(pkgDir, parsed.main));
723-
if (parentVfs.internalModuleStat(mainPath) === 0) {
741+
if (vfsStat(parentVfs, mainPath) === 0) {
724742
const url = pathToFileURL(mainPath).href;
725743
const format = getVFSFormat(parentVfs, mainPath);
726744
return { url, format, shortCircuit: true };
@@ -738,7 +756,7 @@ function resolveBareSpecifier(specifier, context, nextResolve) {
738756
// Resolve subpath like './feature'
739757
const subResolved = normalizeVFSPath(
740758
resolve(pkgDir, packageSubpath));
741-
if (parentVfs.internalModuleStat(subResolved) === 0) {
759+
if (vfsStat(parentVfs, subResolved) === 0) {
742760
const url = pathToFileURL(subResolved).href;
743761
const format = getVFSFormat(parentVfs, subResolved);
744762
return { url, format, shortCircuit: true };
@@ -840,7 +858,7 @@ function vfsLoadHook(url, context, nextLoad) {
840858
const vfs = activeVFSList[i];
841859
if (vfs.shouldHandle(normalized) && vfs.existsSync(normalized)) {
842860
// Only load files, not directories
843-
const statResult = vfs.internalModuleStat(normalized);
861+
const statResult = vfsStat(vfs, normalized);
844862
if (statResult !== 0) {
845863
return nextLoad(url, context);
846864
}

lib/internal/vfs/providers/memory.js

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -195,26 +195,6 @@ class MemoryProvider extends VirtualProvider {
195195
return path.slice(1).split('/');
196196
}
197197

198-
/**
199-
* Gets the parent path.
200-
* @param {string} path Normalized path
201-
* @returns {string|null} Parent path or null for root
202-
*/
203-
#getParentPath(path) {
204-
if (path === '/') {
205-
return null;
206-
}
207-
return pathPosix.dirname(path);
208-
}
209-
210-
/**
211-
* Gets the base name.
212-
* @param {string} path Normalized path
213-
* @returns {string} Base name
214-
*/
215-
#getBaseName(path) {
216-
return pathPosix.basename(path);
217-
}
218198

219199
/**
220200
* Resolves a symlink target to an absolute path.
@@ -227,7 +207,7 @@ class MemoryProvider extends VirtualProvider {
227207
return this.#normalizePath(target);
228208
}
229209
// Relative target: resolve against symlink's parent directory
230-
const parentPath = this.#getParentPath(symlinkPath) || '/';
210+
const parentPath = pathPosix.dirname(symlinkPath);
231211
return this.#normalizePath(pathPosix.join(parentPath, target));
232212
}
233213

@@ -323,10 +303,10 @@ class MemoryProvider extends VirtualProvider {
323303
* @returns {MemoryEntry} The parent directory entry
324304
*/
325305
#ensureParent(path, create, syscall) {
326-
const parentPath = this.#getParentPath(path);
327-
if (parentPath === null) {
306+
if (path === '/') {
328307
return this[kRoot];
329308
}
309+
const parentPath = pathPosix.dirname(path);
330310

331311
const segments = this.#splitPath(parentPath);
332312
let current = this[kRoot];
@@ -458,7 +438,7 @@ class MemoryProvider extends VirtualProvider {
458438
if (err.code !== 'ENOENT' || !isCreate) throw err;
459439
// Create the file
460440
const parent = this.#ensureParent(normalized, true, 'open');
461-
const name = this.#getBaseName(normalized);
441+
const name = pathPosix.basename(normalized);
462442
entry = new MemoryEntry(TYPE_FILE, { mode });
463443
entry.content = Buffer.alloc(0);
464444
parent.children.set(name, entry);
@@ -570,7 +550,7 @@ class MemoryProvider extends VirtualProvider {
570550
}
571551
} else {
572552
const parent = this.#ensureParent(normalized, false, 'mkdir');
573-
const name = this.#getBaseName(normalized);
553+
const name = pathPosix.basename(normalized);
574554
const entry = new MemoryEntry(TYPE_DIR, { mode: options?.mode });
575555
entry.children = new SafeMap();
576556
parent.children.set(name, entry);
@@ -600,7 +580,7 @@ class MemoryProvider extends VirtualProvider {
600580
}
601581

602582
const parent = this.#ensureParent(normalized, false, 'rmdir');
603-
const name = this.#getBaseName(normalized);
583+
const name = pathPosix.basename(normalized);
604584
parent.children.delete(name);
605585
}
606586

@@ -621,7 +601,7 @@ class MemoryProvider extends VirtualProvider {
621601
}
622602

623603
const parent = this.#ensureParent(normalized, false, 'unlink');
624-
const name = this.#getBaseName(normalized);
604+
const name = pathPosix.basename(normalized);
625605
parent.children.delete(name);
626606
}
627607

@@ -642,12 +622,12 @@ class MemoryProvider extends VirtualProvider {
642622

643623
// Remove from old location
644624
const oldParent = this.#ensureParent(normalizedOld, false, 'rename');
645-
const oldName = this.#getBaseName(normalizedOld);
625+
const oldName = pathPosix.basename(normalizedOld);
646626
oldParent.children.delete(oldName);
647627

648628
// Add to new location
649629
const newParent = this.#ensureParent(normalizedNew, true, 'rename');
650-
const newName = this.#getBaseName(normalizedNew);
630+
const newName = pathPosix.basename(normalizedNew);
651631
newParent.children.set(newName, entry);
652632
}
653633

@@ -684,7 +664,7 @@ class MemoryProvider extends VirtualProvider {
684664
}
685665

686666
const parent = this.#ensureParent(normalized, true, 'symlink');
687-
const name = this.#getBaseName(normalized);
667+
const name = pathPosix.basename(normalized);
688668
const entry = new MemoryEntry(TYPE_SYMLINK);
689669
entry.target = target;
690670
parent.children.set(name, entry);

lib/internal/vfs/providers/real.js

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

33
const {
4+
ObjectDefineProperty,
45
Promise,
56
StringPrototypeStartsWith,
67
} = primordials;
@@ -172,6 +173,8 @@ class RealFSProvider extends VirtualProvider {
172173
}
173174
// Resolve to absolute path and normalize
174175
this.#rootPath = path.resolve(rootPath);
176+
ObjectDefineProperty(this, 'readonly', { __proto__: null, value: false });
177+
ObjectDefineProperty(this, 'supportsSymlinks', { __proto__: null, value: true });
175178
}
176179

177180
/**
@@ -182,14 +185,6 @@ class RealFSProvider extends VirtualProvider {
182185
return this.#rootPath;
183186
}
184187

185-
get readonly() {
186-
return false;
187-
}
188-
189-
get supportsSymlinks() {
190-
return true;
191-
}
192-
193188
/**
194189
* Resolves a VFS path to a real filesystem path.
195190
* Ensures the path doesn't escape the root directory.

0 commit comments

Comments
 (0)