Skip to content

Commit a3cb9ac

Browse files
committed
vfs: address PR review comments
- entries.js: use path.join instead of string concatenation - router.js: use primordials (StringPrototypeLastIndexOf, StringPrototypeSplit, StringPrototypeReplaceAll), use path.basename, add comments explaining why some path module functions aren't used (VFS-specific semantics) - fd.js: use numeric separator (10_000), use ??= operator, simplify isVirtualFd - module_hooks.js: move path import to top level, use 'file:' prefix check - sea.js: use kEmptyObject, lazy-load VirtualFileSystem with ??= - mock.js: use path.join, lazy-load VirtualFileSystem with ??=
1 parent 1312520 commit a3cb9ac

File tree

6 files changed

+55
-45
lines changed

6 files changed

+55
-45
lines changed

lib/internal/test_runner/mock/mock.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ const {
5050
const { MockTimers } = require('internal/test_runner/mock/mock_timers');
5151
const { Module } = require('internal/modules/cjs/loader');
5252
const { _load, _nodeModulePaths, _resolveFilename, isBuiltin } = Module;
53+
const { join } = require('path');
54+
55+
// Lazy-load VirtualFileSystem to avoid loading VFS code if fs mocking is not used
56+
let VirtualFileSystem;
5357
function kDefaultFunction() {}
5458
const enableModuleMocking = getOptionValue('--experimental-test-module-mocks');
5559
const kSupportedFormats = [
@@ -454,8 +458,7 @@ class MockFSContext {
454458
* @returns {boolean}
455459
*/
456460
existsSync(path) {
457-
// Prepend prefix to path for VFS lookup
458-
const fullPath = this.#prefix + (StringPrototypeStartsWith(path, '/') ? path : '/' + path);
461+
const fullPath = join(this.#prefix, path);
459462
return this.#vfs.existsSync(fullPath);
460463
}
461464

@@ -806,7 +809,7 @@ class MockTracker {
806809
validateObject(files, 'options.files');
807810
}
808811

809-
const { VirtualFileSystem } = require('internal/vfs/virtual_fs');
812+
VirtualFileSystem ??= require('internal/vfs/virtual_fs').VirtualFileSystem;
810813
const vfs = new VirtualFileSystem({ __proto__: null, moduleHooks: true });
811814

812815
// Add initial files if provided

lib/internal/vfs/entries.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const {
1212
ERR_INVALID_STATE,
1313
},
1414
} = require('internal/errors');
15+
const { join } = require('path');
1516
const { createFileStats, createDirectoryStats } = require('internal/vfs/stats');
1617

1718
// Symbols for private properties
@@ -198,7 +199,7 @@ class VirtualDirectory extends VirtualEntry {
198199
* This is synchronous - the populate callback must be synchronous.
199200
*/
200201
ensurePopulated() {
201-
if (!this[kPopulated] && this[kPopulate] !== null) {
202+
if (!this[kPopulated]) {
202203
const scopedVfs = createScopedVFS(this, (name, entry) => {
203204
this[kEntries].set(name, entry);
204205
});
@@ -274,13 +275,13 @@ function createScopedVFS(directory, addEntry) {
274275
return {
275276
__proto__: null,
276277
addFile(name, content, options) {
277-
const path = directory.path + '/' + name;
278-
const file = new VirtualFile(path, content, options);
278+
const filePath = join(directory.path, name);
279+
const file = new VirtualFile(filePath, content, options);
279280
addEntry(name, file);
280281
},
281282
addDirectory(name, populate, options) {
282-
const path = directory.path + '/' + name;
283-
const dir = new VirtualDirectory(path, populate, options);
283+
const dirPath = join(directory.path, name);
284+
const dir = new VirtualDirectory(dirPath, populate, options);
284285
addEntry(name, dir);
285286
},
286287
};

lib/internal/vfs/fd.js

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const kContent = Symbol('kContent');
1414
const kPath = Symbol('kPath');
1515

1616
// FD range: 10000+ to avoid conflicts with real fds
17-
const VFS_FD_BASE = 10000;
17+
const VFS_FD_BASE = 10_000;
1818
let nextFd = VFS_FD_BASE;
1919

2020
// Global registry of open virtual file descriptors
@@ -92,9 +92,7 @@ class VirtualFD {
9292
* @returns {Buffer}
9393
*/
9494
getContentSync() {
95-
if (this[kContent] === null) {
96-
this[kContent] = this[kEntry].getContentSync();
97-
}
95+
this[kContent] ??= this[kEntry].getContentSync();
9896
return this[kContent];
9997
}
10098

@@ -103,21 +101,11 @@ class VirtualFD {
103101
* @returns {Promise<Buffer>}
104102
*/
105103
async getContent() {
106-
if (this[kContent] === null) {
107-
this[kContent] = await this[kEntry].getContent();
108-
}
104+
this[kContent] ??= await this[kEntry].getContent();
109105
return this[kContent];
110106
}
111107
}
112108

113-
/**
114-
* Allocates a new virtual file descriptor.
115-
* @returns {number}
116-
*/
117-
function allocateFd() {
118-
return nextFd++;
119-
}
120-
121109
/**
122110
* Opens a virtual file and returns its file descriptor.
123111
* @param {VirtualFile} entry The virtual file entry
@@ -126,7 +114,7 @@ function allocateFd() {
126114
* @returns {number} The file descriptor
127115
*/
128116
function openVirtualFd(entry, flags, path) {
129-
const fd = allocateFd();
117+
const fd = nextFd++;
130118
const vfd = new VirtualFD(fd, entry, flags, path);
131119
openFDs.set(fd, vfd);
132120
return fd;
@@ -156,7 +144,7 @@ function closeVirtualFd(fd) {
156144
* @returns {boolean}
157145
*/
158146
function isVirtualFd(fd) {
159-
return fd >= VFS_FD_BASE && openFDs.has(fd);
147+
return openFDs.has(fd);
160148
}
161149

162150
/**

lib/internal/vfs/module_hooks.js

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

11+
const { dirname, resolve } = require('path');
1112
const { normalizePath } = require('internal/vfs/router');
1213
const { pathToFileURL, fileURLToPath, URL } = require('internal/url');
1314
const { createENOENT } = require('internal/vfs/errors');
@@ -307,7 +308,7 @@ function getFormatFromExtension(url) {
307308
* @returns {string} Normalized file path
308309
*/
309310
function urlToPath(urlOrPath) {
310-
if (StringPrototypeStartsWith(urlOrPath, 'file://')) {
311+
if (StringPrototypeStartsWith(urlOrPath, 'file:')) {
311312
return fileURLToPath(urlOrPath);
312313
}
313314
return urlOrPath;
@@ -328,7 +329,7 @@ function vfsResolveHook(specifier, context, nextResolve) {
328329

329330
// Convert specifier to a path we can check
330331
let checkPath;
331-
if (StringPrototypeStartsWith(specifier, 'file://')) {
332+
if (StringPrototypeStartsWith(specifier, 'file:')) {
332333
checkPath = fileURLToPath(specifier);
333334
} else if (specifier[0] === '/') {
334335
// Absolute path
@@ -337,9 +338,8 @@ function vfsResolveHook(specifier, context, nextResolve) {
337338
// Relative path - need to resolve against parent
338339
if (context.parentURL) {
339340
const parentPath = urlToPath(context.parentURL);
340-
const path = require('path');
341-
const parentDir = path.dirname(parentPath);
342-
checkPath = path.resolve(parentDir, specifier);
341+
const parentDir = dirname(parentPath);
342+
checkPath = resolve(parentDir, specifier);
343343
} else {
344344
return nextResolve(specifier, context);
345345
}
@@ -387,8 +387,8 @@ function vfsLoadHook(url, context, nextLoad) {
387387
return nextLoad(url, context);
388388
}
389389

390-
// Only handle file:// URLs
391-
if (!StringPrototypeStartsWith(url, 'file://')) {
390+
// Only handle file: URLs
391+
if (!StringPrototypeStartsWith(url, 'file:')) {
392392
return nextLoad(url, context);
393393
}
394394

@@ -429,6 +429,8 @@ function vfsLoadHook(url, context, nextLoad) {
429429

430430
/**
431431
* Install hooks into Module._stat and various fs functions.
432+
* Note: fs and internal modules are required here (not at top level) to avoid
433+
* circular dependencies during bootstrap. This module may be loaded early.
432434
*/
433435
function installHooks() {
434436
if (hooksInstalled) {

lib/internal/vfs/router.js

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,30 @@
22

33
const {
44
StringPrototypeEndsWith,
5+
StringPrototypeLastIndexOf,
6+
StringPrototypeReplaceAll,
57
StringPrototypeSlice,
8+
StringPrototypeSplit,
69
StringPrototypeStartsWith,
710
} = primordials;
811

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

1114
/**
1215
* Normalizes a path for VFS lookup.
1316
* - Resolves to absolute path
1417
* - Removes trailing slashes (except for root)
15-
* - Normalizes separators to forward slashes
18+
* - Normalizes separators to forward slashes (VFS uses '/' internally)
1619
* @param {string} inputPath The path to normalize
1720
* @returns {string} The normalized path
1821
*/
1922
function normalizePath(inputPath) {
20-
let normalized = path.resolve(inputPath);
23+
let normalized = resolve(inputPath);
2124

22-
// On Windows, convert backslashes to forward slashes for consistent lookup
23-
if (path.sep === '\\') {
24-
normalized = normalized.replace(/\\/g, '/');
25+
// On Windows, convert backslashes to forward slashes for consistent VFS lookup.
26+
// VFS uses forward slashes internally regardless of platform.
27+
if (sep === '\\') {
28+
normalized = StringPrototypeReplaceAll(normalized, '\\', '/');
2529
}
2630

2731
// Remove trailing slash (except for root)
@@ -34,27 +38,29 @@ function normalizePath(inputPath) {
3438

3539
/**
3640
* Splits a path into segments.
41+
* VFS paths are always normalized to use forward slashes.
3742
* @param {string} normalizedPath A normalized absolute path
3843
* @returns {string[]} Path segments
3944
*/
4045
function splitPath(normalizedPath) {
4146
if (normalizedPath === '/') {
4247
return [];
4348
}
44-
// Remove leading slash and split
45-
return StringPrototypeSlice(normalizedPath, 1).split('/');
49+
// Remove leading slash and split by forward slash (VFS internal format)
50+
return StringPrototypeSplit(StringPrototypeSlice(normalizedPath, 1), '/');
4651
}
4752

4853
/**
4954
* Gets the parent path of a normalized path.
55+
* VFS paths are always normalized to use forward slashes.
5056
* @param {string} normalizedPath A normalized absolute path
5157
* @returns {string|null} The parent path, or null if at root
5258
*/
5359
function getParentPath(normalizedPath) {
5460
if (normalizedPath === '/') {
5561
return null;
5662
}
57-
const lastSlash = normalizedPath.lastIndexOf('/');
63+
const lastSlash = StringPrototypeLastIndexOf(normalizedPath, '/');
5864
if (lastSlash === 0) {
5965
return '/';
6066
}
@@ -67,12 +73,14 @@ function getParentPath(normalizedPath) {
6773
* @returns {string} The base name
6874
*/
6975
function getBaseName(normalizedPath) {
70-
const lastSlash = normalizedPath.lastIndexOf('/');
71-
return StringPrototypeSlice(normalizedPath, lastSlash + 1);
76+
// Basename works correctly for VFS paths since they use forward slashes
77+
return basename(normalizedPath);
7278
}
7379

7480
/**
7581
* Checks if a path is under a mount point.
82+
* Note: We don't use path.relative here because VFS mount point semantics
83+
* require exact prefix matching with the forward-slash separator.
7684
* @param {string} normalizedPath A normalized absolute path
7785
* @param {string} mountPoint A normalized mount point path
7886
* @returns {boolean}
@@ -87,6 +95,8 @@ function isUnderMountPoint(normalizedPath, mountPoint) {
8795

8896
/**
8997
* Gets the relative path from a mount point.
98+
* Note: We don't use path.relative here because we need the VFS-internal
99+
* path format (starting with /) for consistent lookup.
90100
* @param {string} normalizedPath A normalized absolute path
91101
* @param {string} mountPoint A normalized mount point path
92102
* @returns {string} The relative path (starting with /)
@@ -100,6 +110,8 @@ function getRelativePath(normalizedPath, mountPoint) {
100110

101111
/**
102112
* Joins a mount point with a relative path.
113+
* Note: We don't use path.join here because VFS relative paths start with /
114+
* and path.join would treat them as absolute paths.
103115
* @param {string} mountPoint A normalized mount point path
104116
* @param {string} relativePath A relative path (starting with /)
105117
* @returns {string} The joined absolute path

lib/internal/vfs/sea.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@ const {
66

77
const { Buffer } = require('buffer');
88
const { isSea, getAsset, getAssetKeys } = require('sea');
9+
const { kEmptyObject } = require('internal/util');
910

1011
// Lazy-loaded VFS
1112
let cachedSeaVfs = null;
1213

14+
// Lazy-load VirtualFileSystem to avoid loading VFS code if not needed
15+
let VirtualFileSystem;
16+
1317
/**
1418
* Creates a VirtualFileSystem populated with SEA assets.
1519
* Assets are mounted at the specified prefix (default: '/sea').
@@ -18,12 +22,12 @@ let cachedSeaVfs = null;
1822
* @param {boolean} [options.moduleHooks] Whether to enable require/import hooks
1923
* @returns {VirtualFileSystem|null} The VFS instance, or null if not running as SEA
2024
*/
21-
function createSeaVfs(options = {}) {
25+
function createSeaVfs(options = kEmptyObject) {
2226
if (!isSea()) {
2327
return null;
2428
}
2529

26-
const { VirtualFileSystem } = require('internal/vfs/virtual_fs');
30+
VirtualFileSystem ??= require('internal/vfs/virtual_fs').VirtualFileSystem;
2731
const prefix = options.prefix ?? '/sea';
2832
const moduleHooks = options.moduleHooks !== false;
2933

0 commit comments

Comments
 (0)