From 66713f6a6c60b88cd86e4175f09a5501019184a5 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 16 Jun 2026 10:39:16 +0200 Subject: [PATCH 01/31] vfs: integrate with CJS and ESM module loaders Co-authored-by: James M Snell Signed-off-by: Matteo Collina --- doc/api/vfs.md | 193 +++++ lib/internal/modules/cjs/loader.js | 38 +- lib/internal/modules/esm/get_format.js | 4 +- lib/internal/modules/esm/load.js | 12 +- lib/internal/modules/esm/resolve.js | 44 +- lib/internal/modules/helpers.js | 214 +++++- lib/internal/modules/package_json_reader.js | 51 +- lib/internal/vfs/file_system.js | 18 + lib/internal/vfs/setup.js | 579 ++++++++++++-- test/parallel/test-vfs-import.mjs | 142 ++++ .../parallel/test-vfs-invalid-package-json.js | 45 ++ test/parallel/test-vfs-layer-id.js | 43 ++ .../parallel/test-vfs-module-hooks-cleanup.js | 115 +++ test/parallel/test-vfs-module-hooks.mjs | 725 ++++++++++++++++++ test/parallel/test-vfs-package-json-cache.js | 23 + test/parallel/test-vfs-package-json.js | 184 +++++ test/parallel/test-vfs-require.js | 405 ++++++++++ test/parallel/test-vfs-scoped-cache-purge.js | 55 ++ 18 files changed, 2802 insertions(+), 88 deletions(-) create mode 100644 test/parallel/test-vfs-import.mjs create mode 100644 test/parallel/test-vfs-invalid-package-json.js create mode 100644 test/parallel/test-vfs-layer-id.js create mode 100644 test/parallel/test-vfs-module-hooks-cleanup.js create mode 100644 test/parallel/test-vfs-module-hooks.mjs create mode 100644 test/parallel/test-vfs-package-json-cache.js create mode 100644 test/parallel/test-vfs-package-json.js create mode 100644 test/parallel/test-vfs-require.js create mode 100644 test/parallel/test-vfs-scoped-cache-purge.js diff --git a/doc/api/vfs.md b/doc/api/vfs.md index 90b8e9c303125a..c7c1f2061b7c68 100644 --- a/doc/api/vfs.md +++ b/doc/api/vfs.md @@ -61,6 +61,11 @@ callback-based, and promise-based file system methods that mirror the shape of the [`node:fs`][] API. All paths are POSIX-style and absolute (starting with `/`). +By default, the file tree is private to the VFS instance. To expose +it through the global `node:fs` module, `require()`, and `import`, +call [`vfs.mount(prefix)`][]; call [`vfs.unmount()`][] (or rely on a +`using` declaration) to detach again. + ## `vfs.create([provider][, options])` + +* `prefix` {string} The path prefix where the VFS will be mounted. +* Returns: {VirtualFileSystem} The VFS instance, for chaining or `using`. + +Mounts the virtual file system at the specified path prefix. After +mounting, files in the VFS can be accessed through the `node:fs` +module — and resolved through `require()` and `import` — using paths +that start with the prefix. + +If a real file-system path already exists at the mount prefix, the +VFS **shadows** that path: every operation against a path under the +mount point is directed to the VFS until the VFS is unmounted. + +```cjs +const vfs = require('node:vfs'); +const fs = require('node:fs'); + +const myVfs = vfs.create(); +myVfs.writeFileSync('/data.txt', 'Hello'); +myVfs.mount('/virtual'); + +fs.readFileSync('/virtual/data.txt', 'utf8'); // 'Hello' +``` + +Each `VirtualFileSystem` instance may be mounted at most once at a +time. Attempting to mount an already-mounted instance throws +`ERR_INVALID_STATE`. Mounting two instances at overlapping prefixes +(e.g., `/virtual` and `/virtual/sub`) also throws `ERR_INVALID_STATE`. + +The VFS supports the [Explicit Resource Management][] proposal. Use +a `using` declaration to unmount automatically when leaving scope: + +```cjs +const vfs = require('node:vfs'); +const fs = require('node:fs'); + +{ + using myVfs = vfs.create(); + myVfs.writeFileSync('/data.txt', 'Hello'); + myVfs.mount('/virtual'); + + fs.readFileSync('/virtual/data.txt', 'utf8'); // 'Hello' +} // VFS is automatically unmounted here + +fs.existsSync('/virtual/data.txt'); // false +``` + +### `vfs.unmount()` + + + +Unmounts the virtual file system. After unmounting, virtual files +are no longer reachable through `node:fs`, `require()`, or `import`. +The same instance may be mounted again, at the same or a different +prefix, by calling `mount()`. + +This method is idempotent: calling `unmount()` on a VFS that is not +currently mounted has no effect. + +### `vfs.mounted` + + + +* {boolean} + +`true` while the VFS is mounted; `false` otherwise. + +### `vfs.mountPoint` + + + +* {string | null} + +The current mount-point path as an absolute string, or `null` when +the VFS is not mounted. + +### `vfs.layerId` + + + +* {number} + +A per-process monotonically increasing identifier assigned at +construction. The id is stable across `mount()` / `unmount()` cycles +for the lifetime of the instance, and is independent of the order in +which VFS layers are mounted. + +The layer id is the building block for cache scoping (see +[Module loader integration][]): + +* it surfaces in `import.meta.url` for ES modules loaded from this + VFS, as a `?vfs-layer=` search parameter, so that the cascaded + loader's caches can be scoped per VFS; +* it appears in the `NODE_DEBUG=vfs` output for `register` and + `deregister` events; +* it appears in the `ERR_INVALID_STATE` error message thrown when two + VFS instances try to mount at overlapping prefixes. + +```cjs +const vfs = require('node:vfs'); + +const a = vfs.create(); +const b = vfs.create(); +console.log(a.layerId); // e.g. 0 +console.log(b.layerId); // a.layerId + 1 +``` + ### `vfs.provider` -* `prefix` {string} The path prefix where the VFS will be mounted. -* Returns: {VirtualFileSystem} The VFS instance, for chaining or `using`. - -Mounts the virtual file system at the specified path prefix. After -mounting, files in the VFS can be accessed through the `node:fs` -module — and resolved through `require()` and `import` — using paths -that start with the prefix. - -If a real file-system path already exists at the mount prefix, the -VFS **shadows** that path: every operation against a path under the -mount point is directed to the VFS until the VFS is unmounted. +* `prefix` {string} A logical name for the mount inside the reserved + VFS namespace. Interpreted as a relative path; leading separators + are ignored. **Default:** `'/'`. +* Returns: {string} The absolute mount point. + +Mounts the virtual file system and returns the resulting mount point. +After mounting, files in the VFS can be accessed through the +`node:fs` module — and resolved through `require()` and `import` — +using paths under the returned mount point. + +Mount points always live inside a reserved namespace, +`${os.devNull}/vfs/layer-/`. Because [`os.devNull`][] is a +character device (POSIX) or a device-namespace path (Windows) that +cannot have child file system entries, no real file-system path can +exist under this namespace: virtual paths never conflate with (or +shadow) real paths, and the layer that owns a path is visible in the +path itself. The `prefix` argument is a purely logical name inside +the namespace — it is never resolved against the working directory, +and a prefix that attempts to escape the namespace (for example with +`..` segments) throws `ERR_INVALID_ARG_VALUE`. ```cjs const vfs = require('node:vfs'); @@ -136,15 +145,17 @@ const fs = require('node:fs'); const myVfs = vfs.create(); myVfs.writeFileSync('/data.txt', 'Hello'); -myVfs.mount('/virtual'); +const mountPoint = myVfs.mount('/virtual'); +// e.g. '/dev/null/vfs/layer-0/virtual' -fs.readFileSync('/virtual/data.txt', 'utf8'); // 'Hello' +fs.readFileSync(`${mountPoint}/data.txt`, 'utf8'); // 'Hello' ``` Each `VirtualFileSystem` instance may be mounted at most once at a time. Attempting to mount an already-mounted instance throws -`ERR_INVALID_STATE`. Mounting two instances at overlapping prefixes -(e.g., `/virtual` and `/virtual/sub`) also throws `ERR_INVALID_STATE`. +`ERR_INVALID_STATE`. Because each instance mounts inside its own +`layer-` namespace, mounts from different instances can +never overlap, even when they use the same `prefix`. The VFS supports the [Explicit Resource Management][] proposal. Use a `using` declaration to unmount automatically when leaving scope: @@ -153,15 +164,16 @@ a `using` declaration to unmount automatically when leaving scope: const vfs = require('node:vfs'); const fs = require('node:fs'); +let mountPoint; { using myVfs = vfs.create(); myVfs.writeFileSync('/data.txt', 'Hello'); - myVfs.mount('/virtual'); + mountPoint = myVfs.mount('/virtual'); - fs.readFileSync('/virtual/data.txt', 'utf8'); // 'Hello' + fs.readFileSync(`${mountPoint}/data.txt`, 'utf8'); // 'Hello' } // VFS is automatically unmounted here -fs.existsSync('/virtual/data.txt'); // false +fs.existsSync(`${mountPoint}/data.txt`); // false ``` ### `vfs.unmount()` @@ -196,8 +208,9 @@ added: REPLACEME * {string | null} -The current mount-point path as an absolute string, or `null` when -the VFS is not mounted. +The current mount point as an absolute string (the value returned by +the last [`vfs.mount(prefix)`][] call), or `null` when the VFS is not +mounted. ### `vfs.layerId` @@ -212,16 +225,10 @@ construction. The id is stable across `mount()` / `unmount()` cycles for the lifetime of the instance, and is independent of the order in which VFS layers are mounted. -The layer id is the building block for cache scoping (see -[Module loader integration][]): - -* it surfaces in `import.meta.url` for ES modules loaded from this - VFS, as a `?vfs-layer=` search parameter, so that the cascaded - loader's caches can be scoped per VFS; -* it appears in the `NODE_DEBUG=vfs` output for `register` and - `deregister` events; -* it appears in the `ERR_INVALID_STATE` error message thrown when two - VFS instances try to mount at overlapping prefixes. +The layer id forms the `layer-` segment of the reserved mount +namespace, so every path served by this instance carries the id, and +it appears in the `NODE_DEBUG=vfs` output for `register` and +`deregister` events. ```cjs const vfs = require('node:vfs'); @@ -322,7 +329,7 @@ The promise namespace mirrors `fs.promises` and includes `readFile`, ## Module loader integration -Once a `VirtualFileSystem` is mounted, paths under the mount prefix +Once a `VirtualFileSystem` is mounted, paths under the mount point participate in module resolution and loading. Both `require()` / `require.resolve()` (CommonJS) and `import` / `import.meta.resolve()` (ECMAScript modules) consult the VFS through @@ -339,44 +346,26 @@ myVfs.mkdirSync('/lib'); myVfs.writeFileSync('/lib/greet.js', 'module.exports = () => "hi";'); myVfs.writeFileSync( '/lib/package.json', '{"main": "./greet.js"}'); -myVfs.mount('/virtual'); +const mountPoint = myVfs.mount('/virtual'); -const greet = require('/virtual/lib'); +const greet = require(`${mountPoint}/lib`); console.log(greet()); // 'hi' myVfs.unmount(); ``` -### Cache scoping and `import.meta.url` - -Module loaders maintain caches that survive the lifetime of any -single VFS. To keep entries from leaking once a VFS is unmounted -without invalidating unrelated real-fs imports, two mechanisms are -combined: - -* **CommonJS caches** (`require.cache`, the internal stat and - realpath caches, and the `package.json` caches) are filtered on - `unmount()`: entries whose absolute filename would be claimed by - the VFS going away are deleted. `__filename` and `module.filename` - are unchanged - they remain plain absolute paths. - -* **ECMAScript module URLs** are tagged at resolve time. When the - resolver determines that a path belongs to a mounted VFS, it - appends `?vfs-layer=` (where `` is the owning instance's - [`vfs.layerId`][]) to the resolved URL. The tag therefore appears - in `import.meta.url` and in cache keys, and on `unmount()` the - cascaded loader's caches drop just the entries that carry the tag - for the unmounting layer. - -```mjs -// inside /virtual/lib/greet.mjs after the VFS above is mounted -console.log(import.meta.url); -// e.g. 'file:///virtual/lib/greet.mjs?vfs-layer=0' -``` +Module identity follows the path: `__filename`, `module.filename`, +and `import.meta.url` are the plain absolute path (or `file:` URL) of +the module under the mount point, with no synthetic decorations. +Importing the same virtual path repeatedly — including through +`import.meta.resolve()` — yields the same module instance, exactly as +for real files. -User code that compares `import.meta.url` literally should account -for the search parameter; use `new URL(import.meta.url).pathname` or -`fileURLToPath()` to obtain the underlying path. +Calling [`vfs.unmount()`][] invalidates the modules that were loaded +from the mount point: a subsequent `require()` or `import` of a path +under a re-created mount re-reads the file from the newly mounted +VFS rather than returning a stale module. Modules loaded from other +VFS instances or from the real file system are unaffected. Mounting and unmounting do not invalidate ESM modules that are already executing. As with any other module-system teardown, @@ -507,7 +496,6 @@ fields use synthetic but stable values: * Times default to the moment the entry was created/last modified. [Explicit Resource Management]: https://github.com/tc39/proposal-explicit-resource-management -[Module loader integration]: #module-loader-integration [`MemoryProvider`]: #class-memoryprovider [`RealFSProvider`]: #class-realfsprovider [`VirtualFileSystem`]: #class-virtualfilesystem @@ -515,6 +503,6 @@ fields use synthetic but stable values: [`fs.BigIntStats`]: fs.md#class-fsbigintstats [`fs.Stats`]: fs.md#class-fsstats [`node:fs`]: fs.md -[`vfs.layerId`]: #vfslayerid +[`os.devNull`]: os.md#osdevnull [`vfs.mount(prefix)`]: #vfsmountprefix [`vfs.unmount()`]: #vfsunmount diff --git a/lib/fs.js b/lib/fs.js index 1ea70ff192d6dd..6f584651a07869 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -2044,8 +2044,13 @@ function fstatSync(fd, options = { __proto__: null, bigint: false }) { function lstatSync(path, options = { __proto__: null, bigint: false, throwIfNoEntry: true }) { const h = vfsState.handlers; if (h !== null) { - const result = h.lstatSync(path, options); - if (result !== undefined) return result; + try { + const result = h.lstatSync(path, options); + if (result !== undefined) return result; + } catch (err) { + if (err?.code === 'ENOENT' && options?.throwIfNoEntry === false) return; + throw err; + } } path = getValidatedPath(path); if (permission.isEnabled() && !permission.has('fs.read', path)) { @@ -2078,8 +2083,13 @@ function lstatSync(path, options = { __proto__: null, bigint: false, throwIfNoEn function statSync(path, options = { __proto__: null, bigint: false, throwIfNoEntry: true }) { const h = vfsState.handlers; if (h !== null) { - const result = h.statSync(path, options); - if (result !== undefined) return result; + try { + const result = h.statSync(path, options); + if (result !== undefined) return result; + } catch (err) { + if (err?.code === 'ENOENT' && options?.throwIfNoEntry === false) return undefined; + throw err; + } } const stats = binding.stat( getValidatedPath(path), diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 44c5a4e2a0e0df..301ce79439ffbb 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -24,6 +24,7 @@ const { ArrayIsArray, ArrayPrototypeFilter, + ArrayPrototypeForEach, ArrayPrototypeIncludes, ArrayPrototypeIndexOf, ArrayPrototypeJoin, @@ -38,6 +39,7 @@ const { Error, FunctionPrototypeCall, JSONParse, + MapPrototypeForEach, ObjectDefineProperty, ObjectFreeze, ObjectGetOwnPropertyDescriptor, @@ -52,7 +54,6 @@ const { RegExpPrototypeExec, SafeMap, SafeSet, - SetPrototypeForEach, String, StringPrototypeCharAt, StringPrototypeCharCodeAt, @@ -114,10 +115,7 @@ const kFormat = Symbol('kFormat'); // Set first due to cycle with ESM loader functions. module.exports = { - clearStatCache, - clearStatCacheForVFS, - purgeModuleCacheForVFS, - setPathCacheWriteRecorder, + purgeModuleCachesForPrefix, kModuleSource, kModuleExport, kModuleExportNames, @@ -292,85 +290,41 @@ function stat(filename) { } /** - * Clear the stat cache. Called when VFS instances are unmounted - * to prevent stale stat results from being returned. Always - * (re)allocates a fresh SafeMap so the cache is non-null on return, - * matching how stat() initializes it on first use. + * Drop every module-loader cache entry (Module._cache, + * Module._pathCache, the stat cache) whose filename lives under + * `mountPoint`. Called by internal/vfs/setup.js when a VFS is + * unmounted. VFS mount points live in a reserved namespace that + * cannot collide with real paths, so a prefix scan is exact: + * real-fs entries and other-VFS entries never match. + * @param {string} mountPoint Absolute mount-point path */ -function clearStatCache() { - statCache = new SafeMap(); -} +function purgeModuleCachesForPrefix(mountPoint) { + const prefix = mountPoint + path.sep; + const owns = (filename) => + typeof filename === 'string' && + (filename === mountPoint || StringPrototypeStartsWith(filename, prefix)); -/** - * Drop only the stat-cache entries owned by the given VFS instance. - * Real-fs entries and entries owned by other VFSes are untouched. - * Iterates the per-VFS ownedFilenames set populated by the loader - * overrides at routing time - O(owned) rather than O(statCache). - * @param {{ownedFilenames: SafeSet}} vfs - */ -function clearStatCacheForVFS(vfs) { - if (statCache === null) { - return; - } - // SafeSetIterator yields filenames; SafeSet's iteration helpers - // protect against pollution of the global Set prototype. - SetPrototypeForEach(vfs.ownedFilenames, (filename) => { - statCache.delete(filename); - }); -} - -/** - * Drop the module and path cache entries owned by the given VFS instance. - * Real-fs entries and entries owned by other VFSes are untouched. This - * keeps Module._cache / Module._pathCache encapsulated so callers don't - * reach into the loader's private data structures directly, and runs - * in O(owned) by walking the per-VFS sets populated at routing time. - * @param {{ownedFilenames: SafeSet, ownedPathCacheKeys: SafeSet}} vfs - */ -function purgeModuleCacheForVFS(vfs) { - // Module._cache: keyed by absolute filename. Iterate only the - // filenames the VFS handled rather than scanning the whole cache. - SetPrototypeForEach(vfs.ownedFilenames, (filename) => { - delete Module._cache[filename]; + // Module._cache: keyed by absolute filename. + ArrayPrototypeForEach(ObjectKeys(Module._cache), (filename) => { + if (owns(filename)) { + delete Module._cache[filename]; + } }); - // Module._pathCache: keyed by "request\0parent" strings. The keys - // are recorded at write time via the pathCache write recorder - // installed by setup.js, so we iterate only the recorded keys. - SetPrototypeForEach(vfs.ownedPathCacheKeys, (key) => { - delete Module._pathCache[key]; + // Module._pathCache: keyed by "request\0parent" strings; the cached + // value is the resolved filename, so filter on the value. + ArrayPrototypeForEach(ObjectKeys(Module._pathCache), (key) => { + if (owns(Module._pathCache[key])) { + delete Module._pathCache[key]; + } }); -} - -/** - * Hook invoked whenever Module._pathCache is written. Allows the VFS - * layer to associate the new pathCache key with the VFS that owns the - * resolved filename, so on unmount the key can be removed by direct - * lookup instead of scanning the whole pathCache. - * @type {((cacheKey: string, resolvedFilename: string) => void) | null} - */ -let pathCacheWriteRecorder = null; -/** - * Install (or clear) the pathCache write recorder. Called by - * internal/vfs/setup.js on hook install / uninstall. - * @param {((cacheKey: string, resolvedFilename: string) => void) | null} fn - */ -function setPathCacheWriteRecorder(fn) { - pathCacheWriteRecorder = fn ?? null; -} - -/** - * Encapsulated writer for Module._pathCache. Goes through the recorder - * so external observers (currently: VFS scope-purge) can track which - * keys belong to which owner without exposing the cache itself. - * @param {string} cacheKey - * @param {string} resolvedFilename - */ -function cachePathResolution(cacheKey, resolvedFilename) { - Module._pathCache[cacheKey] = resolvedFilename; - if (pathCacheWriteRecorder !== null) { - pathCacheWriteRecorder(cacheKey, resolvedFilename); + if (statCache !== null) { + MapPrototypeForEach(statCache, (result, filename) => { + if (owns(filename)) { + statCache.delete(filename); + } + }); } } @@ -965,7 +919,7 @@ Module._findPath = function(request, paths, isMain, conditions = getCjsCondition } if (filename) { - cachePathResolution(cacheKey, filename); + Module._pathCache[cacheKey] = filename; return filename; } @@ -1645,7 +1599,7 @@ Module._resolveFilename = function(request, parent, isMain, options) { if (selfResolved) { const cacheKey = request + '\x00' + (paths.length === 1 ? paths[0] : ArrayPrototypeJoin(paths, '\x00')); - cachePathResolution(cacheKey, selfResolved); + Module._pathCache[cacheKey] = selfResolved; return selfResolved; } diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index b89f3dd4de2fd2..a889071e107df5 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -19,7 +19,6 @@ const { StringPrototypeSlice, StringPrototypeSplit, StringPrototypeStartsWith, - decodeURIComponent, encodeURIComponent, } = primordials; const assert = require('internal/assert'); @@ -47,7 +46,6 @@ const { defaultGetFormatWithoutErrors } = require('internal/modules/esm/get_form const { getConditionsSet } = require('internal/modules/esm/utils'); const packageJsonReader = require('internal/modules/package_json_reader'); const { - loaderGetLayerForPath, loaderLegacyMainResolve, loaderStat, toRealPath, @@ -240,17 +238,9 @@ function finalizeResolution(resolved, base, preserveSymlinks) { try { path = fileURLToPath(resolved); } catch (err) { - // Windows rejects drive-less file URLs such as file:///mnt/app.js before - // loader hooks can see them. If a mounted VFS owns the URL pathname, use - // that pathname as the virtual absolute path; otherwise preserve the - // native file URL validation error. - const pathname = decodeURIComponent(resolved.pathname); - if (loaderGetLayerForPath(pathname) === undefined) { - setOwnProperty(err, 'input', `${resolved}`); - setOwnProperty(err, 'module', `${base}`); - throw err; - } - path = pathname; + setOwnProperty(err, 'input', `${resolved}`); + setOwnProperty(err, 'module', `${base}`); + throw err; } const stats = loaderStat( @@ -296,19 +286,6 @@ function finalizeResolution(resolved, base, preserveSymlinks) { resolved.hash = hash; } - // If the resolved path is owned by an installed VFS layer, append a - // `vfs-layer=N` search param so cache entries are tagged with the - // owning layer. On deregister, entries matching the unmounted layer - // can be scope-purged without touching unrelated real-fs imports. - const layerId = loaderGetLayerForPath(path); - if (layerId !== undefined) { - if (resolved.search) { - resolved.search += `&vfs-layer=${layerId}`; - } else { - resolved.search = `?vfs-layer=${layerId}`; - } - } - return resolved; } diff --git a/lib/internal/modules/helpers.js b/lib/internal/modules/helpers.js index f5d30c0b277319..d4d81fe44a89c2 100644 --- a/lib/internal/modules/helpers.js +++ b/lib/internal/modules/helpers.js @@ -2,12 +2,13 @@ const { ArrayPrototypeForEach, + ArrayPrototypePush, + MapPrototypeForEach, ObjectDefineProperty, ObjectFreeze, ObjectPrototypeHasOwnProperty, SafeMap, SafeSet, - SetPrototypeForEach, StringPrototypeCharCodeAt, StringPrototypeIncludes, StringPrototypeSlice, @@ -70,7 +71,6 @@ let readFileOverride = null; let realpathOverride = null; let legacyMainResolveOverride = null; let getFormatOfExtensionlessFileOverride = null; -let getLayerForPathOverride = null; /** * Set override functions for the module loader's fs operations. @@ -84,22 +84,6 @@ function setLoaderFsOverrides(overrides = kEmptyObject) { realpathOverride = overrides.realpath ?? null; legacyMainResolveOverride = overrides.legacyMainResolve ?? null; getFormatOfExtensionlessFileOverride = overrides.getFormatOfExtensionlessFile ?? null; - getLayerForPathOverride = overrides.getLayerForPath ?? null; -} - -/** - * If `filename` is owned by an installed VFS layer, returns that layer's - * monotonically increasing id; otherwise returns `undefined`. Used by the - * ESM resolver to tag resolved URLs so cache entries can be scope-purged - * on VFS deregister without touching unrelated real-fs imports. - * @param {string} filename Absolute path - * @returns {number|undefined} - */ -function loaderGetLayerForPath(filename) { - if (getLayerForPathOverride !== null) { - return getLayerForPathOverride(filename); - } - return undefined; } /** @@ -193,23 +177,19 @@ function setLoaderPackageOverrides(overrides = kEmptyObject) { } /** - * Clear the realpath cache used by toRealPath's fallback path. - * Called when VFS instances are unmounted so that paths that overlap a - * removed mount are re-resolved against the real filesystem next time. + * Drop realpath-cache entries under an unmounted VFS mount point. + * Entries for unrelated paths are untouched. VFS mount points live in + * a reserved namespace that cannot collide with real paths, so a + * prefix scan on the keys is exact. Called by internal/vfs/setup.js. + * @param {string} mountPoint Absolute mount-point path */ -function clearRealpathCache() { - realpathCache.clear(); -} - -/** - * Drop only realpath-cache entries owned by the given VFS instance. - * Entries for unrelated paths are untouched. Iterates the per-VFS - * ownedFilenames set rather than the whole realpath cache. - * @param {{ownedFilenames: SafeSet}} vfs - */ -function purgeRealpathCacheForVFS(vfs) { - SetPrototypeForEach(vfs.ownedFilenames, (filename) => { - realpathCache.delete(filename); +function purgeRealpathCacheForPrefix(mountPoint) { + const prefix = mountPoint + path.sep; + MapPrototypeForEach(realpathCache, (realPath, requestPath) => { + if (requestPath === mountPoint || + StringPrototypeStartsWith(requestPath, prefix)) { + realpathCache.delete(requestPath); + } }); } @@ -724,8 +704,7 @@ function getRequireStack(parent) { module.exports = { addBuiltinLibsToObject, assertBufferSource, - clearRealpathCache, - purgeRealpathCacheForVFS, + purgeRealpathCacheForPrefix, constants, enableCompileCache, flushCompileCache, @@ -735,7 +714,6 @@ module.exports = { getCompileCacheDir, initializeCjsConditions, loaderGetFormatOfExtensionlessFile, - loaderGetLayerForPath, loaderGetNearestParentPackageJSON, loaderGetPackageScopeConfig, loaderGetPackageType, diff --git a/lib/internal/modules/package_json_reader.js b/lib/internal/modules/package_json_reader.js index 38e6ffc2f4ada4..66ab530470a9bf 100644 --- a/lib/internal/modules/package_json_reader.js +++ b/lib/internal/modules/package_json_reader.js @@ -3,12 +3,13 @@ const { ArrayIsArray, JSONParse, + MapPrototypeForEach, ObjectDefineProperty, RegExpPrototypeExec, SafeMap, - SetPrototypeForEach, StringPrototypeIndexOf, StringPrototypeSlice, + StringPrototypeStartsWith, } = primordials; const { fileURLToPath, @@ -360,28 +361,27 @@ function findPackageJSON(specifier, base = 'data:') { } /** - * Clears all package.json caches. Called by VFS on unmount to prevent - * stale entries from paths that were resolved while a VFS was mounted. + * Drop package.json-cache entries under an unmounted VFS mount point. + * Real-fs entries and other-VFS entries are untouched. VFS mount + * points live in a reserved namespace that cannot collide with real + * paths, so a prefix scan is exact. Called by internal/vfs/setup.js. + * @param {string} mountPoint Absolute mount-point path */ -function clearPackageJSONCache() { - moduleToParentPackageJSONCache.clear(); - deserializedPackageJSONCache.clear(); -} - -/** - * Drop only package.json-cache entries owned by the given VFS instance. - * Real-fs entries and other-VFS entries are untouched. Iterates the - * per-VFS ownedFilenames set (populated at routing time by the loader - * overrides) rather than scanning the global caches. The set is a - * superset of the VFS-owned keys, so the same iteration covers both - * the input-path cache and the package.json-path cache; misses are - * harmless `Map.delete` no-ops. - * @param {{ownedFilenames: SafeSet}} vfs - */ -function purgePackageJSONCacheForVFS(vfs) { - SetPrototypeForEach(vfs.ownedFilenames, (filename) => { - moduleToParentPackageJSONCache.delete(filename); - deserializedPackageJSONCache.delete(filename); +function purgePackageJSONCacheForPrefix(mountPoint) { + const prefix = mountPoint + path.sep; + const owns = (p) => typeof p === 'string' && + (p === mountPoint || StringPrototypeStartsWith(p, prefix)); + // Keyed by module path; the value is the owning package.json path. + MapPrototypeForEach(moduleToParentPackageJSONCache, (pjsonPath, modPath) => { + if (owns(modPath) || owns(pjsonPath)) { + moduleToParentPackageJSONCache.delete(modPath); + } + }); + // Keyed by package.json path. + MapPrototypeForEach(deserializedPackageJSONCache, (config, pjsonPath) => { + if (owns(pjsonPath)) { + deserializedPackageJSONCache.delete(pjsonPath); + } }); } @@ -392,6 +392,5 @@ module.exports = { getPackageType, getPackageJSONURL, findPackageJSON, - clearPackageJSONCache, - purgePackageJSONCacheForVFS, + purgePackageJSONCacheForPrefix, }; diff --git a/lib/internal/vfs/file_system.js b/lib/internal/vfs/file_system.js index d5c1ddc395254a..6846e55881d519 100644 --- a/lib/internal/vfs/file_system.js +++ b/lib/internal/vfs/file_system.js @@ -3,22 +3,23 @@ const { MathRandom, ObjectFreeze, - SafeSet, Symbol, SymbolDispose, } = primordials; const { codes: { + ERR_INVALID_ARG_VALUE, ERR_INVALID_STATE, }, } = require('internal/errors'); -const { validateBoolean } = require('internal/validators'); +const { validateBoolean, validateString } = require('internal/validators'); const { MemoryProvider } = require('internal/vfs/providers/memory'); const path = require('path'); -const { posix: pathPosix, resolve: resolvePath, toNamespacedPath } = path; +const { posix: pathPosix, join: platformJoin, resolve: resolvePath, toNamespacedPath } = path; const { join: joinPath } = pathPosix; const { + getLayerRoot, isUnderMountPoint, getRelativePath, } = require('internal/vfs/router'); @@ -49,12 +50,6 @@ const kNormalizedMountPoint = Symbol('kNormalizedMountPoint'); const kMounted = Symbol('kMounted'); const kPromises = Symbol('kPromises'); const kLayerId = Symbol('kLayerId'); -// Per-VFS sets used by the loader to scope-purge module caches on unmount -// without iterating every cached entry. Populated by the loader overrides -// in internal/vfs/setup.js whenever a request is claimed by this VFS, and -// cleared on unmount once the purge has run. -const kOwnedFilenames = Symbol('kOwnedFilenames'); -const kOwnedPathCacheKeys = Symbol('kOwnedPathCacheKeys'); // Per-process monotonically increasing counter that gives each // VirtualFileSystem instance a unique, stable id. Useful for ordering @@ -127,8 +122,6 @@ class VirtualFileSystem { this[kMounted] = false; this[kPromises] = null; // Lazy-initialized this[kLayerId] = nextLayerId++; - this[kOwnedFilenames] = new SafeSet(); - this[kOwnedPathCacheKeys] = new SafeSet(); } /** @@ -176,21 +169,42 @@ class VirtualFileSystem { // ==================== Mount ==================== /** - * Mounts the VFS at a specific path prefix. - * @param {string} prefix The mount point path - * @returns {VirtualFileSystem} The VFS instance for chaining - */ - mount(prefix) { + * Mounts the VFS inside this instance's reserved namespace and + * returns the resulting mount point. The mount point always lives + * under `${os.devNull}/vfs/layer-/` - a location that + * cannot exist on the real file system (`os.devNull` is a + * character device on POSIX and a device-namespace path on Windows; + * neither can have child filesystem entries) - so virtual paths + * never conflate with real paths and the owning layer is decidable + * from the path alone. + * @param {string} [prefix] Logical prefix appended to the reserved + * namespace. Interpreted as a relative path inside the namespace; + * leading separators are ignored. + * @returns {string} The absolute mount point + */ + mount(prefix = '/') { if (this[kMounted]) { throw new ERR_INVALID_STATE('VFS is already mounted'); } - this[kMountPoint] = resolvePath(prefix); - this[kNormalizedMountPoint] = normalizeMountedPath(this[kMountPoint]); + validateString(prefix, 'prefix'); + const layerRoot = getLayerRoot(this[kLayerId]); + // platformJoin treats an absolute prefix as relative to layerRoot; + // resolvePath drops any trailing separator (e.g. for prefix '/'). + // The containment check rejects '..' segments escaping the + // reserved namespace. + const mountPoint = resolvePath(platformJoin(layerRoot, prefix)); + const normalized = normalizeMountedPath(mountPoint); + if (!isUnderMountPoint(normalized, normalizeMountedPath(layerRoot))) { + throw new ERR_INVALID_ARG_VALUE( + 'prefix', prefix, 'must not escape the VFS namespace'); + } + this[kMountPoint] = mountPoint; + this[kNormalizedMountPoint] = normalized; this[kMounted] = true; - debug('mount %s', this[kMountPoint]); + debug('mount %s', mountPoint); loadVfsSetup(); registerVFS(this); - return this; + return mountPoint; } /** @@ -229,9 +243,8 @@ class VirtualFileSystem { } /** - * Fast path used by the VFS dispatch loops, which can normalize the - * input path once and reuse the result across every active VFS. Saves - * an O(N) path.resolve()+toNamespacedPath() per fs call. + * Fast path used by the VFS dispatch, which normalizes the input + * path once and reuses the result. * @param {string} normalizedInput A path already passed through * normalizeMountedPath. * @returns {boolean} @@ -244,55 +257,6 @@ class VirtualFileSystem { return isUnderMountPoint(normalizedInput, mountPoint); } - /** - * Record an absolute filename that the module loader routed through - * this VFS. The recorded paths are used on unmount to scope-purge - * per-filename loader caches (Module._cache, stat, realpath, - * package.json) without scanning the whole cache. - * @param {string} filename - */ - recordOwnedFilename(filename) { - this[kOwnedFilenames].add(filename); - } - - /** - * Record a Module._pathCache key whose resolved value points into - * this VFS. Used on unmount to scope-purge the path cache by stored - * key rather than scanning every entry. - * @param {string} key - */ - recordOwnedPathCacheKey(key) { - this[kOwnedPathCacheKeys].add(key); - } - - /** - * The set of filenames recorded via recordOwnedFilename. Returned - * directly (not copied) so callers can iterate with the primordials - * iterator helpers used in the loader's cache purge paths. - * @returns {SafeSet} - */ - get ownedFilenames() { - return this[kOwnedFilenames]; - } - - /** - * The set of Module._pathCache keys recorded via - * recordOwnedPathCacheKey. - * @returns {SafeSet} - */ - get ownedPathCacheKeys() { - return this[kOwnedPathCacheKeys]; - } - - /** - * Drop the per-VFS ownership tracking after the loader has used it - * to purge its caches on unmount. - */ - clearOwnedKeys() { - this[kOwnedFilenames].clear(); - this[kOwnedPathCacheKeys].clear(); - } - // ==================== Path Resolution ==================== /** diff --git a/lib/internal/vfs/router.js b/lib/internal/vfs/router.js index b610b271695a3d..3e0780d20b4ec8 100644 --- a/lib/internal/vfs/router.js +++ b/lib/internal/vfs/router.js @@ -2,11 +2,73 @@ const { ArrayPrototypeJoin, + StringPrototypeCharCodeAt, StringPrototypeSplit, StringPrototypeStartsWith, } = primordials; -const { isAbsolute, relative, sep } = require('path'); +const { isAbsolute, relative, resolve, sep, toNamespacedPath } = require('path'); + +// All VFS mount points live under `${os.devNull}/vfs/layer-/`. +// `os.devNull` is a character device on POSIX (`/dev/null`) and a +// device-namespace path on Windows (`\\.\NUL`); neither can have child +// filesystem entries, so no real file-system path can exist under this +// root. Whether a path is governed by a VFS - and by which layer - is +// decidable by looking at the path alone. ASCII-clean by definition, +// which keeps mount points safe to embed in module specifiers even +// when Node.js itself is installed under a directory name that +// contains URL-reserved characters. +let vfsRoot; +let normalizedVfsRoot; + +function getVfsRoot() { + vfsRoot ??= require('os').devNull + sep + 'vfs'; + return vfsRoot; +} + +function getNormalizedVfsRoot() { + normalizedVfsRoot ??= toNamespacedPath(resolve(getVfsRoot())); + return normalizedVfsRoot; +} + +/** + * Returns the reserved namespace root for a VFS layer: + * `${os.devNull}/vfs/layer-`. + * @param {number} layerId + * @returns {string} + */ +function getLayerRoot(layerId) { + return getVfsRoot() + sep + 'layer-' + layerId; +} + +/** + * Extracts the layer id from a normalized path under the VFS root, or + * returns -1 when the path does not carry a well-formed + * `.../vfs/layer-` segment. The caller must have verified that + * `normalizedPath` starts with the normalized VFS root. + * @param {string} normalizedPath + * @returns {number} + */ +function getLayerIdFromPath(normalizedPath) { + // Skip 'layer-'. + const start = getNormalizedVfsRoot().length + 7; + if (!StringPrototypeStartsWith(normalizedPath, sep + 'layer-', + getNormalizedVfsRoot().length)) { + return -1; + } + let id = 0; + let i = start; + const len = normalizedPath.length; + if (i >= len) return -1; + for (; i < len; i++) { + const c = StringPrototypeCharCodeAt(normalizedPath, i); + if (c === 47 || c === 92) break; // '/' or '\\' + if (c < 48 || c > 57) return -1; // not a digit + id = id * 10 + (c - 48); + } + if (i === start) return -1; // 'layer-' with no digits + return id; +} // `path.sep` is required here because on Windows `path.resolve('/virtual')` // produces 'C:\virtual' and all resolved paths use backslashes - a hardcoded @@ -40,6 +102,10 @@ function getRelativePath(normalizedPath, mountPoint) { } module.exports = { + getLayerIdFromPath, + getLayerRoot, + getNormalizedVfsRoot, + getVfsRoot, isUnderMountPoint, getRelativePath, isAbsolutePath: isAbsolute, diff --git a/lib/internal/vfs/setup.js b/lib/internal/vfs/setup.js index 406b87699c1849..a14ac93a2ac674 100644 --- a/lib/internal/vfs/setup.js +++ b/lib/internal/vfs/setup.js @@ -3,24 +3,21 @@ const { ArrayIsArray, ArrayPrototypeForEach, - ArrayPrototypeIndexOf, - ArrayPrototypePush, - ArrayPrototypeSplice, JSONParse, JSONStringify, MapPrototypeForEach, ObjectKeys, PromiseResolve, + SafeMap, String, - StringPrototypeCharAt, StringPrototypeEndsWith, - StringPrototypeIndexOf, StringPrototypeStartsWith, } = primordials; const { Buffer } = require('buffer'); +const { isArrayBufferView } = require('internal/util/types'); const { dirname, join, sep } = require('path'); -const { fileURLToPath, URL } = require('internal/url'); +const { fileURLToPath, pathToFileURL, URL } = require('internal/url'); const { kEmptyObject } = require('internal/util'); const { validateObject } = require('internal/validators'); const { @@ -33,6 +30,11 @@ const { } = require('internal/errors'); const { createENOENT, createEXDEV } = require('internal/vfs/errors'); const { normalizeMountedPath } = require('internal/vfs/file_system'); +const { + getLayerIdFromPath, + getNormalizedVfsRoot, + isUnderMountPoint, +} = require('internal/vfs/router'); const { getVirtualFd, closeVirtualFd } = require('internal/vfs/fd'); const { assertEncoding, setVfsHandlers } = require('internal/fs/utils'); const permission = require('internal/process/permission'); @@ -84,11 +86,17 @@ function writeFileSyncFd(fd, data, options) { return true; } -// Registry of active VFS instances. -const activeVFSList = []; +// Registry of active VFS instances, keyed by layer id. Because every +// mount point embeds its layer id (`${execPath}/vfs/layer-/...`), +// dispatch is a map lookup rather than a scan of mounted layers. +const activeVFSLayers = new SafeMap(); let hooksInstalled = false; let vfsHandlerObj; +// Cached `getNormalizedVfsRoot() + sep`, computed when the first VFS +// registers (not at module scope: this file may be snapshotted and the +// root is discovered lazily from require('os').devNull). +let normalizedVfsRootPrefix = null; function registerVFS(vfs) { if (permission.isEnabled() && !getOptionValue('--allow-fs-vfs')) { @@ -97,117 +105,87 @@ function registerVFS(vfs) { 'Use --allow-fs-vfs to allow it.', ); } - if (ArrayPrototypeIndexOf(activeVFSList, vfs) !== -1) return; - - const newMount = vfs.mountPoint; - if (newMount != null) { - for (let i = 0; i < activeVFSList.length; i++) { - const existing = activeVFSList[i]; - const existingMount = existing.mountPoint; - if (existingMount == null) continue; - // Use path.sep so the trailing-separator guard works on Windows where - // mountPoint values are resolved to drive-letter / backslash paths. - const newPrefix = newMount === sep ? sep : newMount + sep; - const existingPrefix = existingMount === sep ? sep : existingMount + sep; - if (newMount === existingMount || - StringPrototypeStartsWith(newMount, existingPrefix) || - StringPrototypeStartsWith(existingMount, newPrefix)) { - throw new ERR_INVALID_STATE( - `VFS mount '${newMount}' (layer ${vfs.layerId}) overlaps with ` + - `existing mount '${existingMount}' (layer ${existing.layerId})`, - ); - } - } - } - ArrayPrototypePush(activeVFSList, vfs); + if (activeVFSLayers.has(vfs.layerId)) return; + activeVFSLayers.set(vfs.layerId, vfs); debug('register layer=%d mount=%s active=%d', - vfs.layerId, newMount, activeVFSList.length); + vfs.layerId, vfs.mountPoint, activeVFSLayers.size); if (!hooksInstalled) { installHooks(); } } function deregisterVFS(vfs) { - const index = ArrayPrototypeIndexOf(activeVFSList, vfs); - if (index === -1) return; - ArrayPrototypeSplice(activeVFSList, index, 1); - debug('deregister layer=%d active=%d', vfs.layerId, activeVFSList.length); - // Scope-purge: only drop loader-cache entries that belong to the VFS - // that is going away. Other VFSes and real-fs imports are untouched. + if (!activeVFSLayers.delete(vfs.layerId)) return; + debug('deregister layer=%d active=%d', vfs.layerId, activeVFSLayers.size); + // Drop loader-cache entries under the unmounted prefix. Other VFSes + // and real-fs imports are untouched. purgeLoaderCachesForVFS(vfs); - if (activeVFSList.length === 0) { + if (activeVFSLayers.size === 0) { uninstallHooks(); } } /** - * Returns true if `url` carries the `vfs-layer=` tag emitted by - * esm/resolve.js for the given VFS layer. Used to filter ESM cache - * entries on deregister. The match is delimiter-bounded so a tag for - * layer 1 does not also match layer 11, 12, etc: the tag must be - * preceded by `?` or `&` and followed by `&`, `#`, or end-of-string. - * @param {string} url - * @param {number} layerId - * @returns {boolean} + * Resolves a path string to the active VFS that owns it, or null. + * Ownership is decidable from the path alone: all mount points live + * under the reserved `${os.devNull}/vfs/layer-/` namespace, + * so a single prefix comparison rejects every real-file-system path + * and a map lookup finds the owning layer. + * @param {string} inputPath + * @returns {object|null} The owning VirtualFileSystem or null */ -function urlBelongsToLayer(url, layerId) { - if (typeof url !== 'string') return false; - const tag = `vfs-layer=${layerId}`; - const urlLen = url.length; - const tagLen = tag.length; - let i = StringPrototypeIndexOf(url, tag); - while (i !== -1) { - const before = i > 0 ? StringPrototypeCharAt(url, i - 1) : ''; - const afterIdx = i + tagLen; - const after = afterIdx < urlLen ? StringPrototypeCharAt(url, afterIdx) : ''; - if ((before === '?' || before === '&') && - (after === '' || after === '&' || after === '#')) { - return true; - } - i = StringPrototypeIndexOf(url, tag, afterIdx); +function findVFS(inputPath) { + const normalized = normalizeMountedPath(inputPath); + if (!StringPrototypeStartsWith(normalized, normalizedVfsRootPrefix)) { + return null; } - return false; + const layerId = getLayerIdFromPath(normalized); + if (layerId === -1) return null; + const vfs = activeVFSLayers.get(layerId); + if (vfs === undefined || !vfs.shouldHandleNormalized(normalized)) { + return null; + } + return vfs; } /** - * Drop the cache entries owned by `vfs` from the JS-reachable loader - * caches. Real-fs entries and other-VFS entries are left in place. + * Drop the cache entries under `vfs`'s mount point from the + * JS-reachable loader caches. Real-fs entries and other-VFS entries + * are left in place. Because mount points cannot collide with real + * paths - and symlink resolution inside a VFS always yields paths + * under the same mount point - a prefix scan is exact. * @param {object} vfs The VFS being deregistered */ function purgeLoaderCachesForVFS(vfs) { - const layerId = vfs.layerId; + const mountPoint = vfs.mountPoint; - // CJS module / path / stat caches: O(owned). Each loader cache is - // walked by iterating the per-VFS sets recorded at routing time - // (vfs.ownedFilenames, vfs.ownedPathCacheKeys) rather than scanning - // the global caches. const cjsLoader = require('internal/modules/cjs/loader'); - cjsLoader.purgeModuleCacheForVFS(vfs); - cjsLoader.clearStatCacheForVFS(vfs); + cjsLoader.purgeModuleCachesForPrefix(mountPoint); - // Realpath cache used by helpers.toRealPath. Keyed by absolute path. - // Under the normal flow the realpath override claims VFS paths - // before the fallback fs.realpathSync populates this cache, so it - // should typically not contain VFS-owned entries. Kept for the - // mount-over-pre-resolved-path corner case. + // Realpath cache used by helpers.toRealPath, keyed by input path. const helpers = require('internal/modules/helpers'); - helpers.purgeRealpathCacheForVFS(vfs); + helpers.purgeRealpathCacheForPrefix(mountPoint); - // package.json caches: scope-purge by recorded filename / pjson path. + // package.json caches: keyed by module path / package.json path. const pkgReader = require('internal/modules/package_json_reader'); - pkgReader.purgePackageJSONCacheForVFS(vfs); + pkgReader.purgePackageJSONCacheForPrefix(mountPoint); - // ESM cascaded loader: scope-purge by URL tag. + // ESM cascaded loader: purge by file-URL prefix of the mount point. const esmLoader = require('internal/modules/esm/loader'); if (esmLoader.isCascadedLoaderInitialized()) { const loader = esmLoader.getOrInitializeCascadedLoader(); const loadCache = loader.loadCache; + const mountURL = pathToFileURL(mountPoint).href; + const mountURLPrefix = mountURL + '/'; // LoadCache extends SafeMap (url -> { [type]: job }). Iterate via // MapPrototypeForEach (not for-of map.keys()) so a polluted Map // iterator can't break the cleanup path. if (loadCache && typeof loadCache.delete === 'function') { MapPrototypeForEach(loadCache, (variants, url) => { - if (urlBelongsToLayer(url, layerId) && variants) { + if (typeof url === 'string' && + (url === mountURL || + StringPrototypeStartsWith(url, mountURLPrefix)) && + variants) { ArrayPrototypeForEach(ObjectKeys(variants), (type) => { loadCache.delete(url, type); }); @@ -215,9 +193,6 @@ function purgeLoaderCachesForVFS(vfs) { }); } } - - // Release the ownership tracking now that all caches have been purged. - vfs.clearOwnedKeys(); } /** @@ -237,45 +212,32 @@ function vfsStat(vfs, filePath) { } /** - * Checks all active VFS instances for a file/directory stat. + * Finds the VFS owning `filename` and stats it. * @param {string} filename The absolute path to check * @returns {{ vfs: object, result: number }|null} */ function findVFSForStat(filename) { - if (activeVFSList.length === 0) return null; - const normalized = normalizeMountedPath(filename); - for (let i = 0; i < activeVFSList.length; i++) { - const vfs = activeVFSList[i]; - if (vfs.shouldHandleNormalized(normalized)) { - vfs.recordOwnedFilename(filename); - return { vfs, result: vfsStat(vfs, filename) }; - } - } - return null; + const vfs = findVFS(filename); + if (vfs === null) return null; + return { vfs, result: vfsStat(vfs, filename) }; } /** - * Checks all active VFS instances for file content. + * Finds the VFS owning `filename` and reads it. * @param {string} filename The absolute path to read * @param {string|object} options Read options * @returns {{ vfs: object, content: Buffer|string }|null} */ function findVFSForRead(filename, options) { - if (activeVFSList.length === 0) return null; - const normalized = normalizeMountedPath(filename); - for (let i = 0; i < activeVFSList.length; i++) { - const vfs = activeVFSList[i]; - if (vfs.shouldHandleNormalized(normalized)) { - vfs.recordOwnedFilename(filename); - if (vfs.existsSync(filename) && vfsStat(vfs, filename) === 0) { - return { vfs, content: vfs.readFileSync(filename, options) }; - } - // Path inside mount but missing/not-a-file: synthesize ENOENT so the - // loader doesn't fall through and read a real-fs file with the same path. - throw createENOENT('open', filename); - } + const vfs = findVFS(filename); + if (vfs === null) return null; + if (vfs.existsSync(filename) && vfsStat(vfs, filename) === 0) { + return { vfs, content: vfs.readFileSync(filename, options) }; } - return null; + // Path inside mount but missing/not-a-file: synthesize ENOENT. Paths + // under the reserved namespace never exist on the real file system, + // so falling through would produce a confusing real-fs error. + throw createENOENT('open', filename); } /** @@ -343,15 +305,18 @@ function serializePackageJSON(parsed, filePath) { } /** - * Walk up directories in VFS looking for package.json. Always returns an - * object. When a package.json is found `.parsed` is populated; otherwise - * `.sentinel` is the last candidate path checked (highest reached before - * walking past the mount or hitting node_modules) - used as the "not found" - * marker matching the C++ binding's contract for getPackageScopeConfig. - * @param {string} startPath Normalized absolute path to start from + * Walk up directories inside `vfs`'s mount looking for package.json. + * Always returns an object. When a package.json is found `.parsed` is + * populated; otherwise `.sentinel` is the last candidate path checked + * (highest reached before walking past the mount or hitting + * node_modules) - used as the "not found" marker matching the C++ + * binding's contract for getPackageScopeConfig. + * @param {object} vfs The VFS that owns startPath + * @param {string} startPath Absolute path to start from * @returns {{ vfs?: object, pjsonPath?: string, parsed?: object, sentinel: string }} */ -function findVFSPackageJSON(startPath) { +function findVFSPackageJSON(vfs, startPath) { + const normalizedMount = normalizeMountedPath(vfs.mountPoint); let currentDir = dirname(startPath); let lastDir; let sentinel = join(currentDir, 'package.json'); @@ -361,18 +326,20 @@ function findVFSPackageJSON(startPath) { break; } const pjsonPath = join(currentDir, 'package.json'); + if (!isUnderMountPoint(normalizeMountedPath(pjsonPath), normalizedMount)) { + // Walked above the mount point: nothing between the mount point + // and the file-system root can exist (the namespace lives under + // os.devNull, a device that cannot have children). + break; + } sentinel = pjsonPath; - const normalizedPjsonPath = normalizeMountedPath(pjsonPath); - for (let i = 0; i < activeVFSList.length; i++) { - const vfs = activeVFSList[i]; - if (vfs.shouldHandleNormalized(normalizedPjsonPath) && vfsStat(vfs, pjsonPath) === 0) { - try { - const content = vfs.readFileSync(pjsonPath, 'utf8'); - const parsed = JSONParse(content); - return { vfs, pjsonPath, parsed, sentinel: pjsonPath }; - } catch { - // SyntaxError or other errors, continue walking - } + if (vfsStat(vfs, pjsonPath) === 0) { + try { + const content = vfs.readFileSync(pjsonPath, 'utf8'); + const parsed = JSONParse(content); + return { vfs, pjsonPath, parsed, sentinel: pjsonPath }; + } catch { + // SyntaxError or other errors, continue walking } } lastDir = currentDir; @@ -382,44 +349,25 @@ function findVFSPackageJSON(startPath) { } function findVFSForExists(filename) { - if (activeVFSList.length === 0) return null; - const normalized = normalizeMountedPath(filename); - for (let i = 0; i < activeVFSList.length; i++) { - const vfs = activeVFSList[i]; - if (vfs.shouldHandleNormalized(normalized)) { - return { vfs, exists: vfs.existsSync(filename) }; - } - } - return null; + const vfs = findVFS(filename); + if (vfs === null) return null; + return { vfs, exists: vfs.existsSync(filename) }; } function findVFSForPath(filename) { - if (activeVFSList.length === 0) return null; - const normalized = normalizeMountedPath(filename); - for (let i = 0; i < activeVFSList.length; i++) { - const vfs = activeVFSList[i]; - if (vfs.shouldHandleNormalized(normalized)) { - return { vfs, normalized: filename }; - } - } - return null; + const vfs = findVFS(filename); + if (vfs === null) return null; + return { vfs, path: filename }; } // Sync read: check exists first, fall through to ENOENT for mounted VFS. function findVFSWith(filename, syscall, fn) { - if (activeVFSList.length === 0) return undefined; - const normalized = normalizeMountedPath(filename); - for (let i = 0; i < activeVFSList.length; i++) { - const vfs = activeVFSList[i]; - if (vfs.shouldHandleNormalized(normalized)) { - vfs.recordOwnedFilename(filename); - if (vfs.existsSync(filename)) { - return fn(vfs, filename); - } - throw createENOENT(syscall, filename); - } + const vfs = findVFS(filename); + if (vfs === null) return undefined; + if (vfs.existsSync(filename)) { + return fn(vfs, filename); } - return undefined; + throw createENOENT(syscall, filename); } function vfsRead(path, syscall, fn) { @@ -432,7 +380,7 @@ function vfsOp(path, fn) { const pathStr = toPathStr(path); if (pathStr !== null) { const r = findVFSForPath(pathStr); - if (r !== null) return fn(r.vfs, r.normalized); + if (r !== null) return fn(r.vfs, r.path); } return undefined; } @@ -441,22 +389,15 @@ function vfsOpVoid(path, fn) { const pathStr = toPathStr(path); if (pathStr !== null) { const r = findVFSForPath(pathStr); - if (r !== null) { fn(r.vfs, r.normalized); return true; } + if (r !== null) { fn(r.vfs, r.path); return true; } } return undefined; } function checkSameVFS(srcPath, destPath, syscall, srcVfs) { - for (let i = 0; i < activeVFSList.length; i++) { - const vfs = activeVFSList[i]; - if (vfs.shouldHandle(destPath)) { - if (vfs !== srcVfs) { - throw createEXDEV(syscall, srcPath); - } - return; - } + if (findVFS(destPath) !== srcVfs) { + throw createEXDEV(syscall, srcPath); } - throw createEXDEV(syscall, srcPath); } function createVfsHandlers() { @@ -499,26 +440,14 @@ function createVfsHandlers() { lstatSync(path, options) { const pathStr = toPathStr(path); if (pathStr === null) return undefined; - for (let i = 0; i < activeVFSList.length; i++) { - const vfs = activeVFSList[i]; - if (vfs.shouldHandle(pathStr)) { - try { - return vfs.lstatSync(pathStr, options); - } catch (e) { - if (e?.code === 'ENOENT' && options?.throwIfNoEntry === false) return undefined; - throw e; - } - } - } - return undefined; + const vfs = findVFS(pathStr); + if (vfs === null) return undefined; + // Missing entries surface as ENOENT so fs.lstatSync's own + // throwIfNoEntry handling can apply uniformly. + return vfs.lstatSync(pathStr, options); }, statSync(path, options) { - try { - return vfsRead(path, 'stat', (vfs, n) => vfs.statSync(n, options)); - } catch (err) { - if (err?.code === 'ENOENT' && options?.throwIfNoEntry === false) return undefined; - throw err; - } + return vfsRead(path, 'stat', (vfs, n) => vfs.statSync(n, options)); }, realpathSync(path, options) { const result = vfsRead(path, 'realpath', (vfs, n) => vfs.realpathSync(n)); @@ -535,7 +464,7 @@ function createVfsHandlers() { if (mode != null && typeof mode !== 'number') { throw new ERR_INVALID_ARG_TYPE('mode', 'integer', mode); } - r.vfs.accessSync(r.normalized, mode); + r.vfs.accessSync(r.path, mode); return true; } } @@ -544,15 +473,11 @@ function createVfsHandlers() { readlinkSync(path, options) { const pathStr = toPathStr(path); if (pathStr === null) return undefined; - for (let i = 0; i < activeVFSList.length; i++) { - const vfs = activeVFSList[i]; - if (vfs.shouldHandle(pathStr)) { - const result = vfs.readlinkSync(pathStr, options); - if (options?.encoding === 'buffer') return Buffer.from(result); - return result; - } - } - return undefined; + const vfs = findVFS(pathStr); + if (vfs === null) return undefined; + const result = vfs.readlinkSync(pathStr, options); + if (options?.encoding === 'buffer') return Buffer.from(result); + return result; }, statfsSync(path, options) { const pathStr = toPathStr(path); @@ -625,11 +550,9 @@ function createVfsHandlers() { openAsBlob(path, options) { const pathStr = toPathStr(path); if (pathStr !== null) { - for (let i = 0; i < activeVFSList.length; i++) { - const vfs = activeVFSList[i]; - if (vfs.shouldHandle(pathStr) && vfs.existsSync(pathStr)) { - return vfs.openAsBlob(pathStr, options); - } + const vfs = findVFS(pathStr); + if (vfs !== null && vfs.existsSync(pathStr)) { + return vfs.openAsBlob(pathStr, options); } } return undefined; @@ -736,7 +659,7 @@ function createVfsHandlers() { const pathStr = toPathStr(path); if (pathStr !== null) { const r = findVFSForPath(pathStr); - if (r !== null) return r.vfs.createReadStream(r.normalized, options); + if (r !== null) return r.vfs.createReadStream(r.path, options); } return undefined; }, @@ -744,7 +667,7 @@ function createVfsHandlers() { const pathStr = toPathStr(path); if (pathStr !== null) { const r = findVFSForPath(pathStr); - if (r !== null) return r.vfs.createWriteStream(r.normalized, options); + if (r !== null) return r.vfs.createWriteStream(r.path, options); } return undefined; }, @@ -785,13 +708,9 @@ function createVfsHandlers() { lstat(path, options) { const pathStr = toPathStr(path); if (pathStr === null) return undefined; - for (let i = 0; i < activeVFSList.length; i++) { - const vfs = activeVFSList[i]; - if (vfs.shouldHandle(pathStr)) { - return vfs.promises.lstat(pathStr, options); - } - } - return undefined; + const vfs = findVFS(pathStr); + if (vfs === null) return undefined; + return vfs.promises.lstat(pathStr, options); }, stat(path, options) { const promise = vfsOp(path, (vfs, n) => vfs.promises.stat(n, options)); @@ -835,17 +754,13 @@ function createVfsHandlers() { readlink(path, options) { const pathStr = toPathStr(path); if (pathStr === null) return undefined; - for (let i = 0; i < activeVFSList.length; i++) { - const vfs = activeVFSList[i]; - if (vfs.shouldHandle(pathStr)) { - const promise = vfs.promises.readlink(pathStr, options); - if (options?.encoding === 'buffer') { - return promise.then((result) => Buffer.from(result)); - } - return promise; - } + const vfs = findVFS(pathStr); + if (vfs === null) return undefined; + const promise = vfs.promises.readlink(pathStr, options); + if (options?.encoding === 'buffer') { + return promise.then((result) => Buffer.from(result)); } - return undefined; + return promise; }, chown: (path, uid, gid) => vfsOp(path, (vfs, n) => vfs.promises.chown(n, uid, gid).then(() => true)), @@ -932,7 +847,7 @@ function createVfsHandlers() { if (pathStr !== null) { const r = findVFSForPath(pathStr); if (r !== null) { - const fd = r.vfs.openSync(r.normalized, flags, mode); + const fd = r.vfs.openSync(r.path, flags, mode); const vfd = getVirtualFd(fd); return PromiseResolve(vfd.entry); } @@ -973,16 +888,7 @@ function installModuleLoaderOverrides() { return findVFSWith(filename, 'realpath', (vfs, n) => vfs.realpathSync(n)); }, legacyMainResolve(pkgPath, main, base) { - if (activeVFSList.length === 0) return undefined; - const normalizedPkg = normalizeMountedPath(pkgPath); - let handled = false; - for (let i = 0; i < activeVFSList.length; i++) { - if (activeVFSList[i].shouldHandleNormalized(normalizedPkg)) { - handled = true; - break; - } - } - if (!handled) return undefined; + if (findVFS(pkgPath) === null) return undefined; // Extension index mapping (matches C++ legacyMainResolve): // 0-6: try main + extension, then main + /index.ext @@ -1036,62 +942,47 @@ function installModuleLoaderOverrides() { } return 0; // EXTENSIONLESS_FORMAT_JAVASCRIPT }, - getLayerForPath(filename) { - if (activeVFSList.length === 0) return undefined; - const normalized = normalizeMountedPath(filename); - for (let i = 0; i < activeVFSList.length; i++) { - const vfs = activeVFSList[i]; - if (vfs.shouldHandleNormalized(normalized)) return vfs.layerId; - } - return undefined; - }, }); setLoaderPackageOverrides({ readPackageJSON(jsonPath, isESM, base, specifier) { - for (let i = 0; i < activeVFSList.length; i++) { - const vfs = activeVFSList[i]; - if (!vfs.shouldHandle(jsonPath)) continue; - vfs.recordOwnedFilename(jsonPath); - if (vfsStat(vfs, jsonPath) !== 0) return undefined; - let content; - try { - content = vfs.readFileSync(jsonPath, 'utf8'); - } catch { - // Treat read errors as "no package.json" - same as native. - return undefined; - } - let parsed; - try { - parsed = JSONParse(content); - } catch (err) { - // ESM raises ERR_INVALID_PACKAGE_CONFIG on malformed JSON; - // CJS silently ignores it (legacy behavior). - if (isESM) { - throw new ERR_INVALID_PACKAGE_CONFIG(jsonPath, base, err.message); - } - return undefined; + const vfs = findVFS(jsonPath); + if (vfs === null) { + return nativeModulesBinding.readPackageJSON(jsonPath, isESM, base, specifier); + } + if (vfsStat(vfs, jsonPath) !== 0) return undefined; + let content; + try { + content = vfs.readFileSync(jsonPath, 'utf8'); + } catch { + // Treat read errors as "no package.json" - same as native. + return undefined; + } + let parsed; + try { + parsed = JSONParse(content); + } catch (err) { + // ESM raises ERR_INVALID_PACKAGE_CONFIG on malformed JSON; + // CJS silently ignores it (legacy behavior). + if (isESM) { + throw new ERR_INVALID_PACKAGE_CONFIG(jsonPath, base, err.message); } - // serializePackageJSON may throw ERR_INVALID_PACKAGE_CONFIG for - // wrong-type fields - intentionally not caught. - return serializePackageJSON(parsed, jsonPath); + return undefined; } - return nativeModulesBinding.readPackageJSON(jsonPath, isESM, base, specifier); + // serializePackageJSON may throw ERR_INVALID_PACKAGE_CONFIG for + // wrong-type fields - intentionally not caught. + return serializePackageJSON(parsed, jsonPath); }, getNearestParentPackageJSON(checkPath) { - for (let i = 0; i < activeVFSList.length; i++) { - const vfs = activeVFSList[i]; - if (vfs.shouldHandle(checkPath)) { - vfs.recordOwnedFilename(checkPath); - const found = findVFSPackageJSON(checkPath); - if (found.parsed !== undefined) { - vfs.recordOwnedFilename(found.pjsonPath); - return serializePackageJSON(found.parsed, found.pjsonPath); - } - return undefined; - } + const vfs = findVFS(checkPath); + if (vfs === null) { + return nativeModulesBinding.getNearestParentPackageJSON(checkPath); } - return nativeModulesBinding.getNearestParentPackageJSON(checkPath); + const found = findVFSPackageJSON(vfs, checkPath); + if (found.parsed !== undefined) { + return serializePackageJSON(found.parsed, found.pjsonPath); + } + return undefined; }, getPackageScopeConfig(resolved) { let filePath; @@ -1104,21 +995,17 @@ function installModuleLoaderOverrides() { } else { filePath = resolved; } - for (let i = 0; i < activeVFSList.length; i++) { - const vfs = activeVFSList[i]; - if (vfs.shouldHandle(filePath)) { - vfs.recordOwnedFilename(filePath); - const found = findVFSPackageJSON(filePath); - if (found.parsed !== undefined) { - vfs.recordOwnedFilename(found.pjsonPath); - return serializePackageJSON(found.parsed, found.pjsonPath); - } - // No package.json found anywhere up the tree - return the - // topmost path that was checked. Matches the C++ binding contract. - return found.sentinel; - } + const vfs = findVFS(filePath); + if (vfs === null) { + return nativeModulesBinding.getPackageScopeConfig(resolved); + } + const found = findVFSPackageJSON(vfs, filePath); + if (found.parsed !== undefined) { + return serializePackageJSON(found.parsed, found.pjsonPath); } - return nativeModulesBinding.getPackageScopeConfig(resolved); + // No package.json found anywhere up the tree - return the + // topmost path that was checked. Matches the C++ binding contract. + return found.sentinel; }, getPackageType(url) { let filePath; @@ -1131,61 +1018,32 @@ function installModuleLoaderOverrides() { } else { filePath = url; } - for (let i = 0; i < activeVFSList.length; i++) { - const vfs = activeVFSList[i]; - if (vfs.shouldHandle(filePath)) { - vfs.recordOwnedFilename(filePath); - const found = findVFSPackageJSON(filePath); - if (found.parsed !== undefined) { - vfs.recordOwnedFilename(found.pjsonPath); - // Route through serializePackageJSON so a malformed `type` - // (non-string) throws ERR_INVALID_PACKAGE_CONFIG, matching - // the native binding and the other two package.json - // overrides. The serialized tuple is [name, main, type, ...]. - const type = serializePackageJSON(found.parsed, found.pjsonPath)[2]; - if (type === 'module' || type === 'commonjs') return type; - } - return undefined; - } + const vfs = findVFS(filePath); + if (vfs === null) { + return nativeModulesBinding.getPackageType(url); + } + const found = findVFSPackageJSON(vfs, filePath); + if (found.parsed !== undefined) { + // Route through serializePackageJSON so a malformed `type` + // (non-string) throws ERR_INVALID_PACKAGE_CONFIG, matching + // the native binding and the other two package.json + // overrides. The serialized tuple is [name, main, type, ...]. + const type = serializePackageJSON(found.parsed, found.pjsonPath)[2]; + if (type === 'module' || type === 'commonjs') return type; } - return nativeModulesBinding.getPackageType(url); + return undefined; }, }); } -/** - * Record a `Module._pathCache` write against the VFS that owns the - * resolved filename. Installed as the loader's pathCache write - * recorder so on unmount we can delete by recorded key in O(owned) - * rather than scan the whole path cache. - * @param {string} cacheKey - * @param {string} resolvedFilename - */ -function recordPathCacheWrite(cacheKey, resolvedFilename) { - if (typeof resolvedFilename !== 'string') return; - for (let i = 0; i < activeVFSList.length; i++) { - const vfs = activeVFSList[i]; - // ownedFilenames is the authoritative set of paths this VFS has - // already handled. If the resolved filename is in there, the key - // belongs to this VFS. Falling back to vfs.shouldHandle would - // re-normalize the path on every pathCache write - keeping it as - // a Set.has() lookup means the recorder is O(M) Set checks, not - // O(M) path normalizations. - if (vfs.ownedFilenames.has(resolvedFilename)) { - vfs.recordOwnedPathCacheKey(cacheKey); - return; - } - } -} - /** * Install all VFS hooks: module loader overrides and fs handlers. */ function installHooks() { if (hooksInstalled) return; debug('install hooks'); + normalizedVfsRootPrefix = getNormalizedVfsRoot() + sep; installModuleLoaderOverrides(); - require('internal/modules/cjs/loader').setPathCacheWriteRecorder(recordPathCacheWrite); vfsHandlerObj = createVfsHandlers(); setVfsHandlers(vfsHandlerObj); hooksInstalled = true; @@ -1203,7 +1061,6 @@ function uninstallHooks() { require('internal/modules/helpers'); setLoaderFsOverrides(); setLoaderPackageOverrides(); - require('internal/modules/cjs/loader').setPathCacheWriteRecorder(null); setVfsHandlers(null); vfsHandlerObj = undefined; hooksInstalled = false; diff --git a/test/parallel/test-vfs-destructuring.js b/test/parallel/test-vfs-destructuring.js index 11422cf4eda89d..5c2d074cc74615 100644 --- a/test/parallel/test-vfs-destructuring.js +++ b/test/parallel/test-vfs-destructuring.js @@ -18,17 +18,16 @@ const { realpathSync, } = require('fs'); -// path.resolve here so the mount point and the assertion targets are in the -// platform's native form (e.g. 'D:\vfs_destr' on Windows). VirtualFileSystem -// stores the mount point via path.resolve internally, so we mirror that. -const MOUNT = path.resolve('/vfs_destr'); -const FILE = path.join(MOUNT, 'file.txt'); - const myVfs = vfs.create(); myVfs.mkdirSync('/sub', { recursive: true }); myVfs.writeFileSync('/file.txt', 'hello from vfs'); myVfs.writeFileSync('/sub/nested.txt', 'nested content'); -myVfs.mount(MOUNT); + +// mount() returns the actual mount point (under os.devNull/vfs/...); +// '/vfs_destr' is just a logical label. The returned path is already in the +// platform's native form, so assertion targets are built from it directly. +const MOUNT = myVfs.mount('/vfs_destr'); +const FILE = path.join(MOUNT, 'file.txt'); { const content = readFileSync(FILE, 'utf8'); diff --git a/test/parallel/test-vfs-fs-accessSync.js b/test/parallel/test-vfs-fs-accessSync.js index a05bfd13282306..cc2794a5ad7e5a 100644 --- a/test/parallel/test-vfs-fs-accessSync.js +++ b/test/parallel/test-vfs-fs-accessSync.js @@ -9,11 +9,10 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const mountPoint = path.resolve('/tmp/vfs-accessSync-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); -myVfs.mount(mountPoint); +const mountPoint = myVfs.mount('/accessSync'); // Existing path succeeds fs.accessSync(path.join(mountPoint, 'src/hello.txt')); diff --git a/test/parallel/test-vfs-fs-callback-error-paths.js b/test/parallel/test-vfs-fs-callback-error-paths.js index 8a9bf95980e59a..be7506b297ce96 100644 --- a/test/parallel/test-vfs-fs-callback-error-paths.js +++ b/test/parallel/test-vfs-fs-callback-error-paths.js @@ -9,14 +9,11 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const baseMountPoint = path.resolve('/tmp/vfs-cb-err-' + process.pid); -let counter = 0; function mounted() { - const mountPoint = baseMountPoint + '-' + (counter++); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); - myVfs.mount(mountPoint); + const mountPoint = myVfs.mount('/m'); return { myVfs, mountPoint }; } diff --git a/test/parallel/test-vfs-fs-chmod-callback.js b/test/parallel/test-vfs-fs-chmod-callback.js index 72523a3bd7e831..28c6bb4bc5c01d 100644 --- a/test/parallel/test-vfs-fs-chmod-callback.js +++ b/test/parallel/test-vfs-fs-chmod-callback.js @@ -9,11 +9,10 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const mountPoint = path.resolve('/tmp/vfs-chmod-cb-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); -myVfs.mount(mountPoint); +const mountPoint = myVfs.mount('/chmod-cb'); const target = path.join(mountPoint, 'src/hello.txt'); const uid = process.getuid?.() ?? 0; diff --git a/test/parallel/test-vfs-fs-chmodSync.js b/test/parallel/test-vfs-fs-chmodSync.js index f4403d3708726c..0862b7bd3438e3 100644 --- a/test/parallel/test-vfs-fs-chmodSync.js +++ b/test/parallel/test-vfs-fs-chmodSync.js @@ -10,11 +10,10 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const mountPoint = path.resolve('/tmp/vfs-chmodSync-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); -myVfs.mount(mountPoint); +const mountPoint = myVfs.mount('/chmodSync'); const target = path.join(mountPoint, 'src/hello.txt'); const uid = process.getuid?.() ?? 0; diff --git a/test/parallel/test-vfs-fs-copyFileSync.js b/test/parallel/test-vfs-fs-copyFileSync.js index 9f97421329c541..d9b2a9aa81b990 100644 --- a/test/parallel/test-vfs-fs-copyFileSync.js +++ b/test/parallel/test-vfs-fs-copyFileSync.js @@ -9,11 +9,10 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const mountPoint = path.resolve('/tmp/vfs-copyFileSync-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); -myVfs.mount(mountPoint); +const mountPoint = myVfs.mount('/copyFileSync'); fs.copyFileSync( path.join(mountPoint, 'src/hello.txt'), diff --git a/test/parallel/test-vfs-fs-createReadStream.js b/test/parallel/test-vfs-fs-createReadStream.js index 08303a361e1fb8..76b710ee3e5e4b 100644 --- a/test/parallel/test-vfs-fs-createReadStream.js +++ b/test/parallel/test-vfs-fs-createReadStream.js @@ -10,14 +10,11 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const baseMountPoint = path.resolve('/tmp/vfs-createReadStream-' + process.pid); -let counter = 0; function mounted() { - const mountPoint = baseMountPoint + '-' + (counter++); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); - myVfs.mount(mountPoint); + const mountPoint = myVfs.mount('/m'); return { myVfs, mountPoint }; } diff --git a/test/parallel/test-vfs-fs-createWriteStream.js b/test/parallel/test-vfs-fs-createWriteStream.js index 42660acb7f63b7..1d9cf20c07df88 100644 --- a/test/parallel/test-vfs-fs-createWriteStream.js +++ b/test/parallel/test-vfs-fs-createWriteStream.js @@ -10,15 +10,10 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const baseMountPoint = path.resolve( - '/tmp/vfs-createWriteStream-' + process.pid, -); -let counter = 0; function mounted() { - const mountPoint = baseMountPoint + '-' + (counter++); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); - myVfs.mount(mountPoint); + const mountPoint = myVfs.mount('/m'); return { myVfs, mountPoint }; } diff --git a/test/parallel/test-vfs-fs-existsSync.js b/test/parallel/test-vfs-fs-existsSync.js index 190304a154ca42..f351cf7b1fcc49 100644 --- a/test/parallel/test-vfs-fs-existsSync.js +++ b/test/parallel/test-vfs-fs-existsSync.js @@ -9,11 +9,10 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const mountPoint = path.resolve('/tmp/vfs-existsSync-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); -myVfs.mount(mountPoint); +const mountPoint = myVfs.mount('/existsSync'); assert.strictEqual(fs.existsSync(path.join(mountPoint, 'src/hello.txt')), true); assert.strictEqual(fs.existsSync(path.join(mountPoint, 'src')), true); diff --git a/test/parallel/test-vfs-fs-fchmod-callback.js b/test/parallel/test-vfs-fs-fchmod-callback.js index fa55c934f36f4b..9fed2d6cd040ab 100644 --- a/test/parallel/test-vfs-fs-fchmod-callback.js +++ b/test/parallel/test-vfs-fs-fchmod-callback.js @@ -9,11 +9,10 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const mountPoint = path.resolve('/tmp/vfs-fchmod-cb-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); -myVfs.mount(mountPoint); +const mountPoint = myVfs.mount('/fchmod-cb'); const fd = fs.openSync(path.join(mountPoint, 'src/hello.txt'), 'r+'); const uid = process.getuid?.() ?? 0; diff --git a/test/parallel/test-vfs-fs-linkSync.js b/test/parallel/test-vfs-fs-linkSync.js index f8090b24720d92..003a162b99bfdc 100644 --- a/test/parallel/test-vfs-fs-linkSync.js +++ b/test/parallel/test-vfs-fs-linkSync.js @@ -9,11 +9,10 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const mountPoint = path.resolve('/tmp/vfs-linkSync-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); -myVfs.mount(mountPoint); +const mountPoint = myVfs.mount('/linkSync'); fs.linkSync( path.join(mountPoint, 'src/hello.txt'), diff --git a/test/parallel/test-vfs-fs-mkdir-callback.js b/test/parallel/test-vfs-fs-mkdir-callback.js index e354cc94d01b98..000a60cf002c1b 100644 --- a/test/parallel/test-vfs-fs-mkdir-callback.js +++ b/test/parallel/test-vfs-fs-mkdir-callback.js @@ -9,14 +9,11 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const baseMountPoint = path.resolve('/tmp/vfs-mkdir-cb-' + process.pid); -let counter = 0; function mounted() { - const mountPoint = baseMountPoint + '-' + (counter++); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); - myVfs.mount(mountPoint); + const mountPoint = myVfs.mount('/m'); return { myVfs, mountPoint }; } diff --git a/test/parallel/test-vfs-fs-mkdirSync.js b/test/parallel/test-vfs-fs-mkdirSync.js index de3959a06e3b79..b532c331d6f914 100644 --- a/test/parallel/test-vfs-fs-mkdirSync.js +++ b/test/parallel/test-vfs-fs-mkdirSync.js @@ -9,10 +9,9 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const mountPoint = path.resolve('/tmp/vfs-mkdirSync-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); -myVfs.mount(mountPoint); +const mountPoint = myVfs.mount('/mkdirSync'); // Plain mkdir fs.mkdirSync(path.join(mountPoint, 'src/d1')); diff --git a/test/parallel/test-vfs-fs-mkdtempSync.js b/test/parallel/test-vfs-fs-mkdtempSync.js index ba837b716cb6ca..b1cf48e5a7b0a0 100644 --- a/test/parallel/test-vfs-fs-mkdtempSync.js +++ b/test/parallel/test-vfs-fs-mkdtempSync.js @@ -10,10 +10,9 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const mountPoint = path.resolve('/tmp/vfs-mkdtempSync-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); -myVfs.mount(mountPoint); +const mountPoint = myVfs.mount('/mkdtempSync'); const prefix = path.join(mountPoint, 'src/tmp-'); diff --git a/test/parallel/test-vfs-fs-open-callback.js b/test/parallel/test-vfs-fs-open-callback.js index b1cacb11d921d2..75b51a345bb03b 100644 --- a/test/parallel/test-vfs-fs-open-callback.js +++ b/test/parallel/test-vfs-fs-open-callback.js @@ -10,14 +10,11 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const baseMountPoint = path.resolve('/tmp/vfs-open-cb-' + process.pid); -let counter = 0; function mounted() { - const mountPoint = baseMountPoint + '-' + (counter++); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); - myVfs.mount(mountPoint); + const mountPoint = myVfs.mount('/m'); return { myVfs, mountPoint }; } diff --git a/test/parallel/test-vfs-fs-openAsBlob.js b/test/parallel/test-vfs-fs-openAsBlob.js index 1c8d175c8f99c4..83029bfc2232ee 100644 --- a/test/parallel/test-vfs-fs-openAsBlob.js +++ b/test/parallel/test-vfs-fs-openAsBlob.js @@ -9,11 +9,10 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const mountPoint = path.resolve('/tmp/vfs-openAsBlob-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); -myVfs.mount(mountPoint); +const mountPoint = myVfs.mount('/openAsBlob'); fs.openAsBlob(path.join(mountPoint, 'src/hello.txt')) .then(async (blob) => { diff --git a/test/parallel/test-vfs-fs-openSync.js b/test/parallel/test-vfs-fs-openSync.js index f2c73f0d634469..9fcfeecedf0a10 100644 --- a/test/parallel/test-vfs-fs-openSync.js +++ b/test/parallel/test-vfs-fs-openSync.js @@ -12,11 +12,10 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const mountPoint = path.resolve('/tmp/vfs-openSync-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); -myVfs.mount(mountPoint); +const mountPoint = myVfs.mount('/openSync'); // openSync + fstatSync + readSync + closeSync { diff --git a/test/parallel/test-vfs-fs-opendir-callback.js b/test/parallel/test-vfs-fs-opendir-callback.js index 36b8ef81d74ed7..7654a36dbbf925 100644 --- a/test/parallel/test-vfs-fs-opendir-callback.js +++ b/test/parallel/test-vfs-fs-opendir-callback.js @@ -10,15 +10,12 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const baseMountPoint = path.resolve('/tmp/vfs-opendir-cb-' + process.pid); -let counter = 0; function mounted() { - const mountPoint = baseMountPoint + '-' + (counter++); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); myVfs.writeFileSync('/src/data.json', '{}'); - myVfs.mount(mountPoint); + const mountPoint = myVfs.mount('/m'); return { myVfs, mountPoint }; } diff --git a/test/parallel/test-vfs-fs-opendirSync.js b/test/parallel/test-vfs-fs-opendirSync.js index 18ba4c49dd21eb..6eaf91c97efd5a 100644 --- a/test/parallel/test-vfs-fs-opendirSync.js +++ b/test/parallel/test-vfs-fs-opendirSync.js @@ -9,12 +9,11 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const mountPoint = path.resolve('/tmp/vfs-opendirSync-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src/subdir', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); myVfs.writeFileSync('/src/data.json', '{}'); -myVfs.mount(mountPoint); +const mountPoint = myVfs.mount('/opendirSync'); const dir = fs.opendirSync(path.join(mountPoint, 'src')); const names = []; diff --git a/test/parallel/test-vfs-fs-promises-buffer-encoding.js b/test/parallel/test-vfs-fs-promises-buffer-encoding.js index cfb38787ab465c..c2bcefa188e69e 100644 --- a/test/parallel/test-vfs-fs-promises-buffer-encoding.js +++ b/test/parallel/test-vfs-fs-promises-buffer-encoding.js @@ -10,14 +10,11 @@ const fsp = require('fs/promises'); const path = require('path'); const vfs = require('node:vfs'); -const baseMountPoint = path.resolve('/tmp/vfs-buf-enc-' + process.pid); -let counter = 0; function mounted() { - const mountPoint = baseMountPoint + '-' + (counter++); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); - myVfs.mount(mountPoint); + const mountPoint = myVfs.mount('/m'); return { myVfs, mountPoint }; } diff --git a/test/parallel/test-vfs-fs-promises-stat-no-throw.js b/test/parallel/test-vfs-fs-promises-stat-no-throw.js index 6bd0c546a02d8c..9f343fb5ad9ed7 100644 --- a/test/parallel/test-vfs-fs-promises-stat-no-throw.js +++ b/test/parallel/test-vfs-fs-promises-stat-no-throw.js @@ -11,11 +11,10 @@ const path = require('path'); const vfs = require('node:vfs'); (async () => { - const mountPoint = path.resolve('/tmp/vfs-stat-no-throw-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); - myVfs.mount(mountPoint); + const mountPoint = myVfs.mount('/stat-no-throw'); // Missing file -> undefined const missing = await fsp.stat(path.join(mountPoint, 'src/nope'), diff --git a/test/parallel/test-vfs-fs-promises.js b/test/parallel/test-vfs-fs-promises.js index d4b151162f2802..c24ae480a5dd7e 100644 --- a/test/parallel/test-vfs-fs-promises.js +++ b/test/parallel/test-vfs-fs-promises.js @@ -12,11 +12,10 @@ const path = require('path'); const vfs = require('node:vfs'); (async () => { - const mountPoint = path.resolve('/tmp/vfs-promises-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); - myVfs.mount(mountPoint); + const mountPoint = myVfs.mount('/promises'); const p = (s) => path.join(mountPoint, s); // Path-based reads diff --git a/test/parallel/test-vfs-fs-readFile-callback.js b/test/parallel/test-vfs-fs-readFile-callback.js index 5902f399b30a9a..6489d4364dbbc1 100644 --- a/test/parallel/test-vfs-fs-readFile-callback.js +++ b/test/parallel/test-vfs-fs-readFile-callback.js @@ -10,14 +10,11 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const baseMountPoint = path.resolve('/tmp/vfs-readFile-cb-' + process.pid); -let counter = 0; function mounted() { - const mountPoint = baseMountPoint + '-' + (counter++); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); - myVfs.mount(mountPoint); + const mountPoint = myVfs.mount('/m'); return { myVfs, mountPoint }; } diff --git a/test/parallel/test-vfs-fs-readFileSync.js b/test/parallel/test-vfs-fs-readFileSync.js index 1ca906b2a0577c..f496b23b87d2a4 100644 --- a/test/parallel/test-vfs-fs-readFileSync.js +++ b/test/parallel/test-vfs-fs-readFileSync.js @@ -9,11 +9,10 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const mountPoint = path.resolve('/tmp/vfs-readFileSync-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); -myVfs.mount(mountPoint); +const mountPoint = myVfs.mount('/readFileSync'); // Default (buffer) result { @@ -48,15 +47,12 @@ myVfs.unmount(); // is renamed. { const root = path.join('/tmp', 'vfs-real-readFileSync-' + process.pid); - const realMountPoint = path.join('/tmp', 'vfs-real-readFileSync-mount-' + process.pid); fs.rmSync(root, { recursive: true, force: true }); - fs.rmSync(realMountPoint, { recursive: true, force: true }); fs.mkdirSync(root, { recursive: true }); - fs.mkdirSync(realMountPoint, { recursive: true }); - const realVfs = vfs - .create(new vfs.RealFSProvider(root), { emitExperimentalWarning: false }) - .mount(realMountPoint); + const realVfs = vfs.create(new vfs.RealFSProvider(root), + { emitExperimentalWarning: false }); + const realMountPoint = realVfs.mount('/real'); try { fs.writeFileSync(path.join(root, 'a.txt'), 'still readable'); const fd = fs.openSync(path.join(realMountPoint, 'a.txt'), 'r'); @@ -69,6 +65,5 @@ myVfs.unmount(); } finally { realVfs.unmount(); fs.rmSync(root, { recursive: true, force: true }); - fs.rmSync(realMountPoint, { recursive: true, force: true }); } } diff --git a/test/parallel/test-vfs-fs-readdirSync.js b/test/parallel/test-vfs-fs-readdirSync.js index 9a795548133914..1fe629f5f442b0 100644 --- a/test/parallel/test-vfs-fs-readdirSync.js +++ b/test/parallel/test-vfs-fs-readdirSync.js @@ -10,12 +10,11 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const mountPoint = path.resolve('/tmp/vfs-readdirSync-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src/subdir', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); myVfs.writeFileSync('/src/data.json', '{}'); -myVfs.mount(mountPoint); +const mountPoint = myVfs.mount('/readdirSync'); // Default (utf8 string array) { diff --git a/test/parallel/test-vfs-fs-realpathSync.js b/test/parallel/test-vfs-fs-realpathSync.js index 77d0288776161d..53d46cba3a002f 100644 --- a/test/parallel/test-vfs-fs-realpathSync.js +++ b/test/parallel/test-vfs-fs-realpathSync.js @@ -9,11 +9,10 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const mountPoint = path.resolve('/tmp/vfs-realpathSync-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); -myVfs.mount(mountPoint); +const mountPoint = myVfs.mount('/realpathSync'); const p = path.join(mountPoint, 'src/hello.txt'); diff --git a/test/parallel/test-vfs-fs-rename-callback.js b/test/parallel/test-vfs-fs-rename-callback.js index ccf6cccdd56e68..84e2d1d8b0e31d 100644 --- a/test/parallel/test-vfs-fs-rename-callback.js +++ b/test/parallel/test-vfs-fs-rename-callback.js @@ -9,14 +9,11 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const baseMountPoint = path.resolve('/tmp/vfs-rename-cb-' + process.pid); -let counter = 0; function mounted() { - const mountPoint = baseMountPoint + '-' + (counter++); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); - myVfs.mount(mountPoint); + const mountPoint = myVfs.mount('/m'); return { myVfs, mountPoint }; } diff --git a/test/parallel/test-vfs-fs-renameSync.js b/test/parallel/test-vfs-fs-renameSync.js index 88c3f17eef5c83..c15885159d402b 100644 --- a/test/parallel/test-vfs-fs-renameSync.js +++ b/test/parallel/test-vfs-fs-renameSync.js @@ -9,11 +9,10 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const mountPoint = path.resolve('/tmp/vfs-renameSync-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); -myVfs.mount(mountPoint); +const mountPoint = myVfs.mount('/renameSync'); fs.renameSync( path.join(mountPoint, 'src/hello.txt'), diff --git a/test/parallel/test-vfs-fs-rmSync.js b/test/parallel/test-vfs-fs-rmSync.js index a6b77a66bc41d7..b239738df884a2 100644 --- a/test/parallel/test-vfs-fs-rmSync.js +++ b/test/parallel/test-vfs-fs-rmSync.js @@ -9,13 +9,12 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const mountPoint = path.resolve('/tmp/vfs-rmSync-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src/subdir', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); myVfs.writeFileSync('/src/subdir/inside.txt', 'inside'); myVfs.mkdirSync('/empty'); -myVfs.mount(mountPoint); +const mountPoint = myVfs.mount('/rmSync'); // rmdirSync on an empty directory fs.rmdirSync(path.join(mountPoint, 'empty')); diff --git a/test/parallel/test-vfs-fs-stat-callback.js b/test/parallel/test-vfs-fs-stat-callback.js index 873d27e14445ef..de0860593ddd5b 100644 --- a/test/parallel/test-vfs-fs-stat-callback.js +++ b/test/parallel/test-vfs-fs-stat-callback.js @@ -9,14 +9,11 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const baseMountPoint = path.resolve('/tmp/vfs-stat-cb-' + process.pid); -let counter = 0; function mounted() { - const mountPoint = baseMountPoint + '-' + (counter++); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); - myVfs.mount(mountPoint); + const mountPoint = myVfs.mount('/m'); return { myVfs, mountPoint }; } diff --git a/test/parallel/test-vfs-fs-statSync.js b/test/parallel/test-vfs-fs-statSync.js index e952dfab9ad56c..46791319b6ce69 100644 --- a/test/parallel/test-vfs-fs-statSync.js +++ b/test/parallel/test-vfs-fs-statSync.js @@ -10,11 +10,10 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const mountPoint = path.resolve('/tmp/vfs-statSync-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); -myVfs.mount(mountPoint); +const mountPoint = myVfs.mount('/statSync'); // statSync on a regular file { diff --git a/test/parallel/test-vfs-fs-symlink-callback.js b/test/parallel/test-vfs-fs-symlink-callback.js index 004745e0fb973c..e5af84e8d41b47 100644 --- a/test/parallel/test-vfs-fs-symlink-callback.js +++ b/test/parallel/test-vfs-fs-symlink-callback.js @@ -9,11 +9,10 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const mountPoint = path.resolve('/tmp/vfs-symlink-cb-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); -myVfs.mount(mountPoint); +const mountPoint = myVfs.mount('/symlink-cb'); fs.symlink('hello.txt', path.join(mountPoint, 'src/lnk.txt'), common.mustSucceed(() => { diff --git a/test/parallel/test-vfs-fs-symlinkSync.js b/test/parallel/test-vfs-fs-symlinkSync.js index 6d043c59a98ac7..5787a9908f6f57 100644 --- a/test/parallel/test-vfs-fs-symlinkSync.js +++ b/test/parallel/test-vfs-fs-symlinkSync.js @@ -10,11 +10,10 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const mountPoint = path.resolve('/tmp/vfs-symlinkSync-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); -myVfs.mount(mountPoint); +const mountPoint = myVfs.mount('/symlinkSync'); fs.symlinkSync('hello.txt', path.join(mountPoint, 'src/link.txt')); assert.strictEqual( diff --git a/test/parallel/test-vfs-fs-truncate-callback.js b/test/parallel/test-vfs-fs-truncate-callback.js index e33ed7e4d61402..3d218594dc9934 100644 --- a/test/parallel/test-vfs-fs-truncate-callback.js +++ b/test/parallel/test-vfs-fs-truncate-callback.js @@ -9,11 +9,10 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const mountPoint = path.resolve('/tmp/vfs-truncate-cb-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); -myVfs.mount(mountPoint); +const mountPoint = myVfs.mount('/truncate-cb'); fs.truncate(path.join(mountPoint, 'src/hello.txt'), 5, common.mustSucceed(() => { diff --git a/test/parallel/test-vfs-fs-truncateSync.js b/test/parallel/test-vfs-fs-truncateSync.js index 1c15f5647a4716..3ece38fe305f53 100644 --- a/test/parallel/test-vfs-fs-truncateSync.js +++ b/test/parallel/test-vfs-fs-truncateSync.js @@ -9,11 +9,10 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const mountPoint = path.resolve('/tmp/vfs-truncateSync-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); -myVfs.mount(mountPoint); +const mountPoint = myVfs.mount('/truncateSync'); fs.truncateSync(path.join(mountPoint, 'src/hello.txt'), 5); assert.strictEqual( diff --git a/test/parallel/test-vfs-fs-watch-dispatch.js b/test/parallel/test-vfs-fs-watch-dispatch.js index 6d528d6fef7c62..818e5a28a47bf1 100644 --- a/test/parallel/test-vfs-fs-watch-dispatch.js +++ b/test/parallel/test-vfs-fs-watch-dispatch.js @@ -10,11 +10,10 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const mountPoint = path.resolve('/tmp/vfs-watch-dispatch-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); -myVfs.mount(mountPoint); +const mountPoint = myVfs.mount('/watch-dispatch'); const watcher = fs.watch(path.join(mountPoint, 'src/hello.txt')); assert.ok(watcher); diff --git a/test/parallel/test-vfs-fs-writeFile-callback.js b/test/parallel/test-vfs-fs-writeFile-callback.js index a02dc686b0e0a6..c3af45120403fb 100644 --- a/test/parallel/test-vfs-fs-writeFile-callback.js +++ b/test/parallel/test-vfs-fs-writeFile-callback.js @@ -9,13 +9,10 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const baseMountPoint = path.resolve('/tmp/vfs-writeFile-cb-' + process.pid); -let counter = 0; function mounted() { - const mountPoint = baseMountPoint + '-' + (counter++); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); - myVfs.mount(mountPoint); + const mountPoint = myVfs.mount('/m'); return { myVfs, mountPoint }; } diff --git a/test/parallel/test-vfs-fs-writeFileSync.js b/test/parallel/test-vfs-fs-writeFileSync.js index eeed2a685f08e3..fed015b6c94ee2 100644 --- a/test/parallel/test-vfs-fs-writeFileSync.js +++ b/test/parallel/test-vfs-fs-writeFileSync.js @@ -9,10 +9,9 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const mountPoint = path.resolve('/tmp/vfs-writeFileSync-' + process.pid); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); -myVfs.mount(mountPoint); +const mountPoint = myVfs.mount('/writeFileSync'); const target = path.join(mountPoint, 'src/new.txt'); @@ -58,15 +57,12 @@ myVfs.unmount(); // after the backing path is renamed. { const root = path.join('/tmp', 'vfs-real-writeFileSync-' + process.pid); - const realMountPoint = path.join('/tmp', 'vfs-real-writeFileSync-mount-' + process.pid); fs.rmSync(root, { recursive: true, force: true }); - fs.rmSync(realMountPoint, { recursive: true, force: true }); fs.mkdirSync(root, { recursive: true }); - fs.mkdirSync(realMountPoint, { recursive: true }); - const realVfs = vfs - .create(new vfs.RealFSProvider(root), { emitExperimentalWarning: false }) - .mount(realMountPoint); + const realVfs = vfs.create( + new vfs.RealFSProvider(root), { emitExperimentalWarning: false }); + const realMountPoint = realVfs.mount('/real-writeFileSync'); try { const mountedFile = path.join(realMountPoint, 'a.txt'); fs.writeFileSync(path.join(root, 'a.txt'), 'old'); @@ -83,6 +79,5 @@ myVfs.unmount(); } finally { realVfs.unmount(); fs.rmSync(root, { recursive: true, force: true }); - fs.rmSync(realMountPoint, { recursive: true, force: true }); } } diff --git a/test/parallel/test-vfs-import.mjs b/test/parallel/test-vfs-import.mjs index 9b5a661d1a4b0d..21fd0d1a717d73 100644 --- a/test/parallel/test-vfs-import.mjs +++ b/test/parallel/test-vfs-import.mjs @@ -3,17 +3,17 @@ import '../common/index.mjs'; import assert from 'assert'; import vfs from 'node:vfs'; -// NOTE: Each test uses a unique mount path because ESM imports are cached -// by URL — unmounting does not clear the V8 module cache, so reusing a -// mount path would return stale cached modules from earlier tests. +// NOTE: ESM imports are cached by URL and unmounting does not clear the V8 +// module cache. Each vfs.create() gets its own layer id, so every mount +// point returned by mount() is unique — no stale cache entries can be hit. // Test importing a simple virtual ES module { const myVfs = vfs.create(); myVfs.writeFileSync('/hello.mjs', 'export const message = "hello from vfs";'); - myVfs.mount('/esm-named'); + const mountPoint = myVfs.mount('/esm-named'); - const { message } = await import('/esm-named/hello.mjs'); + const { message } = await import(`${mountPoint}/hello.mjs`); assert.strictEqual(message, 'hello from vfs'); myVfs.unmount(); @@ -23,26 +23,28 @@ import vfs from 'node:vfs'; { const myVfs = vfs.create(); myVfs.writeFileSync('/default.mjs', 'export default { name: "test", value: 42 };'); - myVfs.mount('/esm-default'); + const mountPoint = myVfs.mount('/esm-default'); - const mod = await import('/esm-default/default.mjs'); + const mod = await import(`${mountPoint}/default.mjs`); assert.strictEqual(mod.default.name, 'test'); assert.strictEqual(mod.default.value, 42); myVfs.unmount(); } -// Test importing a virtual module that imports another virtual module +// Test importing a virtual module that imports another virtual module. +// Mount first so the embedded absolute specifier can use the mount point. { const myVfs = vfs.create(); - myVfs.writeFileSync('/utils.mjs', 'export function add(a, b) { return a + b; }'); - myVfs.writeFileSync('/main.mjs', ` - import { add } from '/esm-chain/utils.mjs'; + const mountPoint = myVfs.mount('/esm-chain'); + myVfs.writeFileSync(`${mountPoint}/utils.mjs`, + 'export function add(a, b) { return a + b; }'); + myVfs.writeFileSync(`${mountPoint}/main.mjs`, ` + import { add } from ${JSON.stringify(`${mountPoint}/utils.mjs`)}; export const result = add(10, 20); `); - myVfs.mount('/esm-chain'); - const { result } = await import('/esm-chain/main.mjs'); + const { result } = await import(`${mountPoint}/main.mjs`); assert.strictEqual(result, 30); myVfs.unmount(); @@ -57,9 +59,9 @@ import vfs from 'node:vfs'; import { helper } from './helper.mjs'; export const output = helper(); `); - myVfs.mount('/esm-relative'); + const mountPoint = myVfs.mount('/esm-relative'); - const { output } = await import('/esm-relative/lib/index.mjs'); + const { output } = await import(`${mountPoint}/lib/index.mjs`); assert.strictEqual(output, 'helped'); myVfs.unmount(); @@ -69,9 +71,9 @@ import vfs from 'node:vfs'; { const myVfs = vfs.create(); myVfs.writeFileSync('/data.json', JSON.stringify({ items: [1, 2, 3], enabled: true })); - myVfs.mount('/esm-json'); + const mountPoint = myVfs.mount('/esm-json'); - const data = await import('/esm-json/data.json', { with: { type: 'json' } }); + const data = await import(`${mountPoint}/data.json`, { with: { type: 'json' } }); assert.deepStrictEqual(data.default.items, [1, 2, 3]); assert.strictEqual(data.default.enabled, true); @@ -96,15 +98,15 @@ import vfs from 'node:vfs'; const myVfs = vfs.create(); myVfs.writeFileSync('/esm-module.mjs', 'export const esmValue = "esm";'); myVfs.writeFileSync('/cjs-module.js', 'module.exports = { cjsValue: "cjs" };'); - myVfs.mount('/esm-mixed'); + const mountPoint = myVfs.mount('/esm-mixed'); - const { esmValue } = await import('/esm-mixed/esm-module.mjs'); + const { esmValue } = await import(`${mountPoint}/esm-module.mjs`); assert.strictEqual(esmValue, 'esm'); // CJS require should also work (via createRequire) const { createRequire } = await import('module'); const require = createRequire(import.meta.url); - const { cjsValue } = require('/esm-mixed/cjs-module.js'); + const { cjsValue } = require(`${mountPoint}/cjs-module.js`); assert.strictEqual(cjsValue, 'cjs'); myVfs.unmount(); @@ -133,9 +135,9 @@ import vfs from 'node:vfs'; '/app/entry.mjs', "export { fromVfs } from 'my-vfs-pkg';", ); - myVfs.mount('/esm-bare'); + const mountPoint = myVfs.mount('/esm-bare'); - const { fromVfs } = await import('/esm-bare/app/entry.mjs'); + const { fromVfs } = await import(`${mountPoint}/app/entry.mjs`); assert.strictEqual(fromVfs, true); myVfs.unmount(); diff --git a/test/parallel/test-vfs-invalid-package-json.js b/test/parallel/test-vfs-invalid-package-json.js index 89c9bc23cb71d1..17d0e88fafa4ad 100644 --- a/test/parallel/test-vfs-invalid-package-json.js +++ b/test/parallel/test-vfs-invalid-package-json.js @@ -12,9 +12,9 @@ const vfs = require('node:vfs'); myVfs.mkdirSync('/pkg', { recursive: true }); myVfs.writeFileSync('/pkg/package.json', '{ invalid json'); myVfs.writeFileSync('/pkg/index.js', 'module.exports = 42;'); - myVfs.mount('/mnt'); + const mountPoint = myVfs.mount('/mnt'); - assert.strictEqual(require('/mnt/pkg'), 42); + assert.strictEqual(require(`${mountPoint}/pkg`), 42); myVfs.unmount(); } @@ -25,9 +25,9 @@ const vfs = require('node:vfs'); myVfs.mkdirSync('/pkg2', { recursive: true }); myVfs.writeFileSync('/pkg2/package.json', '{"main": "lib.js"}'); myVfs.writeFileSync('/pkg2/lib.js', 'module.exports = 99;'); - myVfs.mount('/mnt2'); + const mountPoint = myVfs.mount('/mnt2'); - assert.strictEqual(require('/mnt2/pkg2'), 99); + assert.strictEqual(require(`${mountPoint}/pkg2`), 99); myVfs.unmount(); } @@ -37,9 +37,9 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/pkg3', { recursive: true }); myVfs.writeFileSync('/pkg3/index.js', 'module.exports = 77;'); - myVfs.mount('/mnt3'); + const mountPoint = myVfs.mount('/mnt3'); - assert.strictEqual(require('/mnt3/pkg3'), 77); + assert.strictEqual(require(`${mountPoint}/pkg3`), 77); myVfs.unmount(); } diff --git a/test/parallel/test-vfs-layer-id.js b/test/parallel/test-vfs-layer-id.js index 4ed6cec6e2380f..a52211425396cd 100644 --- a/test/parallel/test-vfs-layer-id.js +++ b/test/parallel/test-vfs-layer-id.js @@ -3,10 +3,12 @@ // `vfs.layerId` is a per-process monotonically increasing identifier // assigned at construction. It is stable across mount/unmount cycles -// and surfaces in overlap error messages. +// and forms the layer segment of the reserved mount namespace. require('../common'); const assert = require('assert'); +const os = require('os'); +const path = require('path'); const vfs = require('node:vfs'); { @@ -24,20 +26,13 @@ const vfs = require('node:vfs'); assert.strictEqual(a.layerId, idBefore); } -// layerId appears in the overlap error message so the user can tell -// which instance lost the race. +// layerId is the layer segment of the mount point, so the owning layer +// of any VFS path is visible in the path itself. { - const outer = vfs.create(); - const inner = vfs.create(); - outer.mount('/mnt-layer-overlap'); - assert.throws( - () => inner.mount('/mnt-layer-overlap/sub'), - (err) => { - assert.strictEqual(err.code, 'ERR_INVALID_STATE'); - assert.match(err.message, new RegExp(`layer ${inner.layerId}`)); - assert.match(err.message, new RegExp(`layer ${outer.layerId}`)); - return true; - }, - ); - outer.unmount(); + const v = vfs.create(); + const mountPoint = v.mount('/data'); + assert.strictEqual( + mountPoint, + path.join(os.devNull, 'vfs', `layer-${v.layerId}`, 'data')); + v.unmount(); } diff --git a/test/parallel/test-vfs-layer-tag-prefix.mjs b/test/parallel/test-vfs-layer-tag-prefix.mjs index f129d6ca36458f..3d2fe27dc83f9e 100644 --- a/test/parallel/test-vfs-layer-tag-prefix.mjs +++ b/test/parallel/test-vfs-layer-tag-prefix.mjs @@ -1,11 +1,9 @@ // Flags: --experimental-vfs -// Regression: urlBelongsToLayer must do a delimiter-bounded match on -// the `vfs-layer=N` URL tag emitted by esm/resolve.js. With a plain -// substring match, unmounting layer 1 would also drop ESM load-cache -// entries belonging to layer 10, 11, etc, because "vfs-layer=1" is a -// prefix of "vfs-layer=10". The fix anchors the match on `?`/`&` -// before and `&`/`#`/end-of-string after, so unrelated layers survive. +// Layer identity is carried by the mount-point path itself +// (`${execPath}/vfs/layer-/...`), so unmounting one layer must +// purge exactly that layer's ESM cache entries - layers whose ids +// share a decimal prefix (1 vs 10) must be unaffected. import '../common/index.mjs'; import assert from 'node:assert'; @@ -13,8 +11,8 @@ import vfs from 'node:vfs'; // nextLayerId starts at 0 in a fresh process and increments per // VirtualFileSystem construction. Burn through constructions until we -// have one VFS at layer 1 and one at layer 10 - the prefix collision -// the bug needs to be exercised. +// have one VFS at layer 1 and one at layer 10 - the decimal-prefix +// collision this test exercises. const sentinel = vfs.create(); assert.strictEqual(sentinel.layerId, 0); @@ -31,32 +29,27 @@ assert.ok(String(layerTen.layerId).startsWith(String(layerOne.layerId)), 'test scaffolding: 10 must have 1 as a string prefix'); layerOne.writeFileSync('/m.mjs', 'export const tag = "layer-one";'); -layerOne.mount('/mnt-tag-1'); +const mountOne = layerOne.mount('/mnt-tag'); layerTen.writeFileSync('/m.mjs', 'export const tag = "layer-ten";'); -layerTen.mount('/mnt-tag-10'); +const mountTen = layerTen.mount('/mnt-tag'); -// Warm both ESM cache entries. Their URLs will carry -// `?vfs-layer=1` and `?vfs-layer=10` respectively. -const oneA = await import('/mnt-tag-1/m.mjs'); -const tenA = await import('/mnt-tag-10/m.mjs'); +// Warm both ESM cache entries. +const oneA = await import(`${mountOne}/m.mjs`); +const tenA = await import(`${mountTen}/m.mjs`); assert.strictEqual(oneA.tag, 'layer-one'); assert.strictEqual(tenA.tag, 'layer-ten'); -// Unmount layer 1. Under the old substring match this also wrongly -// dropped layer 10's entry; under the fix it survives untouched. +// Unmount layer 1. Layer 10's cache entry must survive: its mount +// point is `.../layer-10/mnt-tag`, which is not under +// `.../layer-1/mnt-tag` even though "1" is a decimal prefix of "10". layerOne.unmount(); -// Layer 10 is still mounted; importing again should resolve to the -// same already-evaluated module namespace. A cache miss would cause -// the loader to create a new module job and re-evaluate, producing a -// different namespace object - that is precisely what the bug caused. -// With the substring-match bug, the loader would create a new module -// job for the second import (since the prior entry was wrongly -// purged), producing a fresh namespace object. With the -// delimiter-bounded match, the cache survives and the second import -// returns the very same namespace. -const tenB = await import('/mnt-tag-10/m.mjs'); +// Layer 10 is still mounted; importing again must resolve to the same +// already-evaluated module namespace. A cache miss would cause the +// loader to create a new module job and re-evaluate, producing a +// different namespace object. +const tenB = await import(`${mountTen}/m.mjs`); assert.strictEqual(tenA, tenB); layerTen.unmount(); diff --git a/test/parallel/test-vfs-module-hooks-cleanup.js b/test/parallel/test-vfs-module-hooks-cleanup.js index acf15fbb25e762..1386a73f8c7f33 100644 --- a/test/parallel/test-vfs-module-hooks-cleanup.js +++ b/test/parallel/test-vfs-module-hooks-cleanup.js @@ -14,14 +14,14 @@ const vfs = require('node:vfs'); { const a = vfs.create(); a.writeFileSync('/m1.js', 'module.exports = "first"'); - a.mount('/mnt-cycle-1'); - assert.strictEqual(require('/mnt-cycle-1/m1.js'), 'first'); + const mountA = a.mount('/mnt-cycle-1'); + assert.strictEqual(require(`${mountA}/m1.js`), 'first'); a.unmount(); const b = vfs.create(); b.writeFileSync('/m2.js', 'module.exports = "second"'); - b.mount('/mnt-cycle-2'); - assert.strictEqual(require('/mnt-cycle-2/m2.js'), 'second'); + const mountB = b.mount('/mnt-cycle-2'); + assert.strictEqual(require(`${mountB}/m2.js`), 'second'); b.unmount(); } @@ -30,8 +30,8 @@ const vfs = require('node:vfs'); { const v = vfs.create(); v.writeFileSync('/x.js', 'module.exports = 1'); - v.mount('/mnt-cleanup'); - require('/mnt-cleanup/x.js'); + const mountPoint = v.mount('/mnt-cleanup'); + require(`${mountPoint}/x.js`); v.unmount(); const fs = require('fs'); assert.strictEqual(typeof fs.readFileSync, 'function'); @@ -45,9 +45,9 @@ const vfs = require('node:vfs'); v.mkdirSync('/pkg'); v.writeFileSync('/pkg/package.json', 'null'); v.writeFileSync('/pkg/index.js', 'module.exports = 1'); - v.mount('/mnt-null-pjson'); + const mountPoint = v.mount('/mnt-null-pjson'); assert.throws( - () => require('/mnt-null-pjson/pkg'), + () => require(`${mountPoint}/pkg`), { code: 'ERR_INVALID_PACKAGE_CONFIG' }, ); v.unmount(); @@ -61,28 +61,28 @@ const vfs = require('node:vfs'); v.mkdirSync('/pkg'); v.writeFileSync('/pkg/package.json', '{"main": 42}'); v.writeFileSync('/pkg/index.js', 'module.exports = "via-index"'); - v.mount('/mnt-lax-main'); - assert.strictEqual(require('/mnt-lax-main/pkg'), 'via-index'); + const mountPoint = v.mount('/mnt-lax-main'); + assert.strictEqual(require(`${mountPoint}/pkg`), 'via-index'); v.unmount(); } // 5) Partial deregister of a multi-mount setup leaves the still-mounted // VFS fully functional. Guards against the prior "nuke caches before -// checking activeVFSList.length === 0" sledgehammer. +// checking the active-layer count" sledgehammer. { const a = vfs.create(); a.writeFileSync('/a.js', 'module.exports = "a"'); - a.mount('/mnt-multi-a'); + const mountA = a.mount('/mnt-multi-a'); const b = vfs.create(); b.writeFileSync('/b.js', 'module.exports = "b"'); - b.mount('/mnt-multi-b'); + const mountB = b.mount('/mnt-multi-b'); - assert.strictEqual(require('/mnt-multi-a/a.js'), 'a'); - assert.strictEqual(require('/mnt-multi-b/b.js'), 'b'); + assert.strictEqual(require(`${mountA}/a.js`), 'a'); + assert.strictEqual(require(`${mountB}/b.js`), 'b'); // Deregister one; the other must still resolve. a.unmount(); - assert.strictEqual(require('/mnt-multi-b/b.js'), 'b'); + assert.strictEqual(require(`${mountB}/b.js`), 'b'); b.unmount(); } @@ -100,9 +100,9 @@ const vfs = require('node:vfs'); v.writeFileSync( '/app/node_modules/badpkg/package.json', '{"main": "./nope.js"}'); v.writeFileSync('/app/entry.mjs', "import 'badpkg';"); - v.mount('/mnt-legacy-err'); + const mountPoint = v.mount('/mnt-legacy-err'); await assert.rejects( - () => import('/mnt-legacy-err/app/entry.mjs'), + () => import(`${mountPoint}/app/entry.mjs`), (err) => { assert.strictEqual(err.code, 'ERR_MODULE_NOT_FOUND'); assert.match(err.message, /nope\.js/); @@ -113,3 +113,24 @@ const vfs = require('node:vfs'); ); v.unmount(); })().then(common.mustCall()); + +// 7) Symlink inside a VFS: the loader resolves through the link, and +// unmount purges the cache entries recorded under the resolved +// realpath too (the realpath of a VFS file always stays under the +// mount point). Remounting the same instance at the same prefix +// reuses the same mount point, so a stale entry would be revived. +{ + const v = vfs.create(); + v.mkdirSync('/real', { recursive: true }); + v.writeFileSync('/real/mod.js', 'module.exports = "one"'); + v.symlinkSync('/real/mod.js', '/link.js'); + const mountPoint = v.mount('/mnt-symlink'); + assert.strictEqual(require(`${mountPoint}/link.js`), 'one'); + v.unmount(); + + v.writeFileSync('/real/mod.js', 'module.exports = "two"'); + const mountPoint2 = v.mount('/mnt-symlink'); + assert.strictEqual(mountPoint2, mountPoint); + assert.strictEqual(require(`${mountPoint2}/link.js`), 'two'); + v.unmount(); +} diff --git a/test/parallel/test-vfs-module-hooks.mjs b/test/parallel/test-vfs-module-hooks.mjs index 2e511f9bac8e09..fc0bdabb014203 100644 --- a/test/parallel/test-vfs-module-hooks.mjs +++ b/test/parallel/test-vfs-module-hooks.mjs @@ -2,12 +2,15 @@ import '../common/index.mjs'; import assert from 'assert'; import { createRequire } from 'module'; +import { pathToFileURL } from 'node:url'; import vfs from 'node:vfs'; const require = createRequire(import.meta.url); -// NOTE: Each test uses a different mount path (/mh1, /mh2, etc.) -// because ESM imports are cached by URL. +// NOTE: mount() takes a purely logical prefix and returns the actual mount +// point (a reserved path under os.devNull). Each test captures the +// returned mount point and uses it for requires/imports. Distinct prefixes +// (/mh1, /mh2, ...) are kept for readability. // ================================================================= // Test: CJS bare specifier resolution with exports string shorthand @@ -27,9 +30,9 @@ const require = createRequire(import.meta.url); '/app/entry.js', "module.exports = require('str-pkg');", ); - myVfs.mount('/mh1'); + const mountPoint = myVfs.mount('/mh1'); - const result = require('/mh1/app/entry.js'); + const result = require(`${mountPoint}/app/entry.js`); assert.strictEqual(result.strExport, true); myVfs.unmount(); @@ -71,14 +74,14 @@ const require = createRequire(import.meta.url); '/app/cjs-entry.js', "module.exports = require('cond-pkg');", ); - myVfs.mount('/mh2'); + const mountPoint = myVfs.mount('/mh2'); // ESM import should get the 'import' condition - const esmResult = await import('/mh2/app/esm-entry.mjs'); + const esmResult = await import(`${mountPoint}/app/esm-entry.mjs`); assert.strictEqual(esmResult.source, 'esm'); // CJS require should get the 'require' condition - const cjsResult = require('/mh2/app/cjs-entry.js'); + const cjsResult = require(`${mountPoint}/app/cjs-entry.js`); assert.strictEqual(cjsResult.source, 'cjs'); myVfs.unmount(); @@ -113,9 +116,9 @@ const require = createRequire(import.meta.url); export { main, feature }; `, ); - myVfs.mount('/mh3'); + const mountPoint = myVfs.mount('/mh3'); - const result = await import('/mh3/app/entry.mjs'); + const result = await import(`${mountPoint}/app/entry.mjs`); assert.strictEqual(result.main, true); assert.strictEqual(result.feature, true); @@ -149,9 +152,9 @@ const require = createRequire(import.meta.url); '/app/entry2.mjs', "export { fromSubCond } from 'sub-cond-pkg';", ); - myVfs.mount('/mh4'); + const mountPoint = myVfs.mount('/mh4'); - const result = await import('/mh4/app/entry2.mjs'); + const result = await import(`${mountPoint}/app/entry2.mjs`); assert.strictEqual(result.fromSubCond, 'esm'); myVfs.unmount(); @@ -189,9 +192,9 @@ const require = createRequire(import.meta.url); '/app/entry3.mjs', "export { nested } from 'nested-cond-pkg';", ); - myVfs.mount('/mh5'); + const mountPoint = myVfs.mount('/mh5'); - const result = await import('/mh5/app/entry3.mjs'); + const result = await import(`${mountPoint}/app/entry3.mjs`); assert.strictEqual(result.nested, 'node-esm'); myVfs.unmount(); @@ -216,9 +219,9 @@ const require = createRequire(import.meta.url); '/app/entry4.js', "module.exports = require('main-pkg');", ); - myVfs.mount('/mh6'); + const mountPoint = myVfs.mount('/mh6'); - const result = require('/mh6/app/entry4.js'); + const result = require(`${mountPoint}/app/entry4.js`); assert.strictEqual(result.fromMain, true); myVfs.unmount(); @@ -241,9 +244,9 @@ const require = createRequire(import.meta.url); '/app/entry5.js', "module.exports = require('index-pkg');", ); - myVfs.mount('/mh7'); + const mountPoint = myVfs.mount('/mh7'); - const result = require('/mh7/app/entry5.js'); + const result = require(`${mountPoint}/app/entry5.js`); assert.strictEqual(result.fromIndex, true); myVfs.unmount(); @@ -254,16 +257,18 @@ const require = createRequire(import.meta.url); // ================================================================= { const myVfs = vfs.create(); - myVfs.mkdirSync('/lib', { recursive: true }); + // The embedded specifier must be an absolute mounted path, so mount + // first and write the files through the mounted paths. + const mountPoint = myVfs.mount('/mh8'); + myVfs.mkdirSync(`${mountPoint}/lib`, { recursive: true }); + myVfs.writeFileSync(`${mountPoint}/lib/utils.js`, 'module.exports = { ext: "js" };'); // File without .js extension in specifier - myVfs.writeFileSync('/lib/utils.js', 'module.exports = { ext: "js" };'); myVfs.writeFileSync( - '/lib/main.js', - "module.exports = require('/mh8/lib/utils');", + `${mountPoint}/lib/main.js`, + `module.exports = require(${JSON.stringify(`${mountPoint}/lib/utils`)});`, ); - myVfs.mount('/mh8'); - const result = require('/mh8/lib/main.js'); + const result = require(`${mountPoint}/lib/main.js`); assert.strictEqual(result.ext, 'js'); myVfs.unmount(); @@ -274,14 +279,16 @@ const require = createRequire(import.meta.url); // ================================================================= { const myVfs = vfs.create(); - myVfs.writeFileSync('/data.json', JSON.stringify({ ext: 'json' })); + // The embedded specifier must be an absolute mounted path, so mount + // first and write the files through the mounted paths. + const mountPoint = myVfs.mount('/mh9'); + myVfs.writeFileSync(`${mountPoint}/data.json`, JSON.stringify({ ext: 'json' })); myVfs.writeFileSync( - '/reader.js', - "module.exports = require('/mh9/data');", + `${mountPoint}/reader.js`, + `module.exports = require(${JSON.stringify(`${mountPoint}/data`)});`, ); - myVfs.mount('/mh9'); - const result = require('/mh9/reader.js'); + const result = require(`${mountPoint}/reader.js`); assert.strictEqual(result.ext, 'json'); myVfs.unmount(); @@ -306,9 +313,9 @@ const require = createRequire(import.meta.url); '/app/scoped-entry.mjs', "export { scoped } from '@myorg/mylib';", ); - myVfs.mount('/mh11'); + const mountPoint = myVfs.mount('/mh11'); - const result = await import('/mh11/app/scoped-entry.mjs'); + const result = await import(`${mountPoint}/app/scoped-entry.mjs`); assert.strictEqual(result.scoped, true); myVfs.unmount(); @@ -342,9 +349,9 @@ const require = createRequire(import.meta.url); export { helpers }; `, ); - myVfs.mount('/mh12'); + const mountPoint = myVfs.mount('/mh12'); - const result = await import('/mh12/app/scoped-sub-entry.mjs'); + const result = await import(`${mountPoint}/app/scoped-sub-entry.mjs`); assert.strictEqual(result.helpers, true); myVfs.unmount(); @@ -357,9 +364,9 @@ const require = createRequire(import.meta.url); const myVfs = vfs.create(); myVfs.writeFileSync('/package.json', JSON.stringify({ type: 'module' })); myVfs.writeFileSync('/mod.js', 'export const fromModule = true;'); - myVfs.mount('/mh13'); + const mountPoint = myVfs.mount('/mh13'); - const result = await import('/mh13/mod.js'); + const result = await import(`${mountPoint}/mod.js`); assert.strictEqual(result.fromModule, true); myVfs.unmount(); @@ -370,20 +377,22 @@ const require = createRequire(import.meta.url); // ================================================================= { const myVfs = vfs.create(); - myVfs.writeFileSync('/package.json', JSON.stringify({ type: 'module' })); - myVfs.writeFileSync('/helper.cjs', 'module.exports = { cjsAlways: true };'); + // The embedded specifier must be an absolute mounted path, so mount + // first and write the files through the mounted paths. + const mountPoint = myVfs.mount('/mh14'); + myVfs.writeFileSync(`${mountPoint}/package.json`, JSON.stringify({ type: 'module' })); + myVfs.writeFileSync(`${mountPoint}/helper.cjs`, 'module.exports = { cjsAlways: true };'); myVfs.writeFileSync( - '/use-cjs.js', + `${mountPoint}/use-cjs.js`, ` import { createRequire } from 'module'; const require = createRequire(import.meta.url); - const result = require('/mh14/helper.cjs'); + const result = require(${JSON.stringify(`${mountPoint}/helper.cjs`)}); export const cjsAlways = result.cjsAlways; `, ); - myVfs.mount('/mh14'); - const result = await import('/mh14/use-cjs.js'); + const result = await import(`${mountPoint}/use-cjs.js`); assert.strictEqual(result.cjsAlways, true); myVfs.unmount(); @@ -395,9 +404,9 @@ const require = createRequire(import.meta.url); { const myVfs = vfs.create(); myVfs.writeFileSync('/fileurl.mjs', 'export const fromFileUrl = true;'); - myVfs.mount('/mh15'); + const mountPoint = myVfs.mount('/mh15'); - const result = await import('file:///mh15/fileurl.mjs'); + const result = await import(pathToFileURL(`${mountPoint}/fileurl.mjs`).href); assert.strictEqual(result.fromFileUrl, true); myVfs.unmount(); @@ -417,10 +426,10 @@ const require = createRequire(import.meta.url); '/dir-pkg/entry.js', 'module.exports = { dirPkg: true };', ); - myVfs.mount('/mh16'); + const mountPoint = myVfs.mount('/mh16'); // Main field has no extension - tryExtensions should resolve entry → entry.js - const result = require('/mh16/dir-pkg'); + const result = require(`${mountPoint}/dir-pkg`); assert.strictEqual(result.dirPkg, true); myVfs.unmount(); @@ -443,9 +452,9 @@ const require = createRequire(import.meta.url); '/app/entry-sub.js', "module.exports = require('direct-pkg/lib/util.js');", ); - myVfs.mount('/mh17'); + const mountPoint = myVfs.mount('/mh17'); - const result = require('/mh17/app/entry-sub.js'); + const result = require(`${mountPoint}/app/entry-sub.js`); assert.strictEqual(result.directSub, true); myVfs.unmount(); @@ -469,9 +478,9 @@ const require = createRequire(import.meta.url); // No .js extension - should be resolved by tryExtensions "module.exports = require('ext-pkg/lib/util');", ); - myVfs.mount('/mh18'); + const mountPoint = myVfs.mount('/mh18'); - const result = require('/mh18/app/entry-ext.js'); + const result = require(`${mountPoint}/app/entry-ext.js`); assert.strictEqual(result.extSub, true); myVfs.unmount(); @@ -495,9 +504,9 @@ const require = createRequire(import.meta.url); '/app/entry-main-ext.js', "module.exports = require('main-ext-pkg');", ); - myVfs.mount('/mh19'); + const mountPoint = myVfs.mount('/mh19'); - const result = require('/mh19/app/entry-main-ext.js'); + const result = require(`${mountPoint}/app/entry-main-ext.js`); assert.strictEqual(result.mainExt, true); myVfs.unmount(); @@ -524,10 +533,10 @@ const require = createRequire(import.meta.url); '/app/entry-arr.js', "module.exports = require('arr-exp-pkg');", ); - myVfs.mount('/mh22'); + const mountPoint = myVfs.mount('/mh22'); // Array target in exports: canonical resolver tries each entry in order - const result = require('/mh22/app/entry-arr.js'); + const result = require(`${mountPoint}/app/entry-arr.js`); assert.strictEqual(result.arrExport, true); myVfs.unmount(); @@ -556,10 +565,10 @@ const require = createRequire(import.meta.url); '/app/entry-default.mjs', "export { fromDefault } from 'default-pkg';", ); - myVfs.mount('/mh23'); + const mountPoint = myVfs.mount('/mh23'); // 'browser' condition not active in Node, 'default' should match - const result = await import('/mh23/app/entry-default.mjs'); + const result = await import(`${mountPoint}/app/entry-default.mjs`); assert.strictEqual(result.fromDefault, true); myVfs.unmount(); @@ -572,9 +581,9 @@ const require = createRequire(import.meta.url); const myVfs = vfs.create(); myVfs.writeFileSync('/package.json', JSON.stringify({ type: 'commonjs' })); myVfs.writeFileSync('/explicit-cjs.js', 'module.exports = { explicitCjs: true };'); - myVfs.mount('/mh24'); + const mountPoint = myVfs.mount('/mh24'); - const result = require('/mh24/explicit-cjs.js'); + const result = require(`${mountPoint}/explicit-cjs.js`); assert.strictEqual(result.explicitCjs, true); myVfs.unmount(); @@ -586,9 +595,9 @@ const require = createRequire(import.meta.url); { const myVfs = vfs.create(); myVfs.writeFileSync('/no-pkg.js', 'module.exports = { noPkg: true };'); - myVfs.mount('/mh25'); + const mountPoint = myVfs.mount('/mh25'); - const result = require('/mh25/no-pkg.js'); + const result = require(`${mountPoint}/no-pkg.js`); assert.strictEqual(result.noPkg, true); myVfs.unmount(); @@ -607,10 +616,10 @@ const require = createRequire(import.meta.url); '/node_modules/inner/index.js', 'module.exports = { inner: true };', ); - myVfs.mount('/mh26'); + const mountPoint = myVfs.mount('/mh26'); // The walk should stop at node_modules, not inherit type:module from root - const result = require('/mh26/node_modules/inner/index.js'); + const result = require(`${mountPoint}/node_modules/inner/index.js`); assert.strictEqual(result.inner, true); myVfs.unmount(); @@ -627,10 +636,10 @@ const require = createRequire(import.meta.url); '/bad-json-dir/index.js', 'module.exports = { fallbackIndex: true };', ); - myVfs.mount('/mh28'); + const mountPoint = myVfs.mount('/mh28'); // Should fall through to index.js after failing to parse package.json - const result = require('/mh28/bad-json-dir'); + const result = require(`${mountPoint}/bad-json-dir`); assert.strictEqual(result.fallbackIndex, true); myVfs.unmount(); @@ -643,10 +652,10 @@ const require = createRequire(import.meta.url); const myVfs = vfs.create(); myVfs.writeFileSync('/package.json', '{ broken json }'); myVfs.writeFileSync('/no-type.js', 'module.exports = { noType: true };'); - myVfs.mount('/mh29'); + const mountPoint = myVfs.mount('/mh29'); // Should treat as 'none' (commonjs) since package.json is invalid - const result = require('/mh29/no-type.js'); + const result = require(`${mountPoint}/no-type.js`); assert.strictEqual(result.noType, true); myVfs.unmount(); @@ -670,9 +679,9 @@ const require = createRequire(import.meta.url); '/app/entry-solo.js', "module.exports = require('@solo/pkg');", ); - myVfs.mount('/mh30'); + const mountPoint = myVfs.mount('/mh30'); - const result = require('/mh30/app/entry-solo.js'); + const result = require(`${mountPoint}/app/entry-solo.js`); assert.strictEqual(result.solo, true); myVfs.unmount(); @@ -687,9 +696,9 @@ const require = createRequire(import.meta.url); import path from 'node:path'; export const sep = path.sep; `); - myVfs.mount('/mh31'); + const mountPoint = myVfs.mount('/mh31'); - const result = await import('/mh31/use-builtin.mjs'); + const result = await import(`${mountPoint}/use-builtin.mjs`); assert.strictEqual(typeof result.sep, 'string'); myVfs.unmount(); @@ -701,9 +710,9 @@ const require = createRequire(import.meta.url); { const myVfs = vfs.create(); myVfs.writeFileSync('/data.json', JSON.stringify({ preformat: true })); - myVfs.mount('/mh32'); + const mountPoint = myVfs.mount('/mh32'); - const result = await import('/mh32/data.json', { with: { type: 'json' } }); + const result = await import(`${mountPoint}/data.json`, { with: { type: 'json' } }); assert.strictEqual(result.default.preformat, true); myVfs.unmount(); @@ -715,10 +724,10 @@ const require = createRequire(import.meta.url); { const myVfs = vfs.create(); myVfs.writeFileSync('/data.txt', 'module.exports = { txt: true };'); - myVfs.mount('/mh33'); + const mountPoint = myVfs.mount('/mh33'); // .txt extension → falls back to 'commonjs' via VFS_FORMAT_MAP default - const result = require('/mh33/data.txt'); + const result = require(`${mountPoint}/data.txt`); assert.strictEqual(result.txt, true); myVfs.unmount(); diff --git a/test/parallel/test-vfs-mount-errors.js b/test/parallel/test-vfs-mount-errors.js index 905f1c6d0683ab..cfcb4a189a4ff6 100644 --- a/test/parallel/test-vfs-mount-errors.js +++ b/test/parallel/test-vfs-mount-errors.js @@ -3,30 +3,25 @@ // Error paths in the VFS mount layer: // - EXDEV when renaming/linking across different VFS instances or VFS<->real -// - lastunmount handler cleanup (vfsState.handlers becomes null again) -// - rename of root mount point is rejected as overlapping +// - last-unmount handler cleanup (vfsState.handlers becomes null again) +// - namespace isolation between layers require('../common'); const assert = require('assert'); const fs = require('fs'); const path = require('path'); +const os = require('os'); const vfs = require('node:vfs'); const { vfsState } = require('internal/fs/utils'); -const baseMountPoint = path.resolve('/tmp/vfs-mount-errors-' + process.pid); -let mountCounter = 0; -const nextMount = () => baseMountPoint + '-' + (mountCounter++); - // EXDEV: rename across two different VFS instances { - const mountA = nextMount(); - const mountB = nextMount(); const a = vfs.create(); const b = vfs.create(); a.writeFileSync('/file.txt', 'a'); b.mkdirSync('/x', { recursive: true }); - a.mount(mountA); - b.mount(mountB); + const mountA = a.mount('/a'); + const mountB = b.mount('/b'); assert.throws( () => fs.renameSync(path.join(mountA, 'file.txt'), @@ -39,14 +34,12 @@ const nextMount = () => baseMountPoint + '-' + (mountCounter++); // EXDEV: copyFileSync across two different VFS instances { - const mountA = nextMount(); - const mountB = nextMount(); const a = vfs.create(); const b = vfs.create(); a.writeFileSync('/file.txt', 'a'); b.mkdirSync('/x', { recursive: true }); - a.mount(mountA); - b.mount(mountB); + const mountA = a.mount('/a'); + const mountB = b.mount('/b'); assert.throws( () => fs.copyFileSync(path.join(mountA, 'file.txt'), @@ -59,14 +52,12 @@ const nextMount = () => baseMountPoint + '-' + (mountCounter++); // EXDEV: linkSync across two different VFS instances { - const mountA = nextMount(); - const mountB = nextMount(); const a = vfs.create(); const b = vfs.create(); a.writeFileSync('/file.txt', 'a'); b.mkdirSync('/x', { recursive: true }); - a.mount(mountA); - b.mount(mountB); + const mountA = a.mount('/a'); + const mountB = b.mount('/b'); assert.throws( () => fs.linkSync(path.join(mountA, 'file.txt'), @@ -79,10 +70,9 @@ const nextMount = () => baseMountPoint + '-' + (mountCounter++); // EXDEV: rename from VFS to a real-fs path { - const mountA = nextMount(); const a = vfs.create(); a.writeFileSync('/file.txt', 'a'); - a.mount(mountA); + const mountA = a.mount('/a'); const tmpReal = '/tmp/vfs-mount-real-' + process.pid + '.txt'; assert.throws( @@ -96,25 +86,25 @@ const nextMount = () => baseMountPoint + '-' + (mountCounter++); { assert.strictEqual(vfsState.handlers, null); const x = vfs.create(); - x.mount(nextMount()); + x.mount('/x'); assert.notStrictEqual(vfsState.handlers, null); x.unmount(); assert.strictEqual(vfsState.handlers, null); // And it re-installs on a subsequent mount const y = vfs.create(); - y.mount(nextMount()); + y.mount('/y'); assert.notStrictEqual(vfsState.handlers, null); y.unmount(); assert.strictEqual(vfsState.handlers, null); } -// Two parallel non-overlapping mounts both register, last-out clears handlers +// Two parallel mounts both register, last-out clears handlers { const a = vfs.create(); const b = vfs.create(); - a.mount(nextMount()); - b.mount(nextMount()); + a.mount('/a'); + b.mount('/b'); assert.notStrictEqual(vfsState.handlers, null); a.unmount(); assert.notStrictEqual(vfsState.handlers, null); @@ -122,38 +112,29 @@ const nextMount = () => baseMountPoint + '-' + (mountCounter++); assert.strictEqual(vfsState.handlers, null); } -// Overlap detection: nested-under and parent-of both rejected +// Namespace isolation: a path in an unmounted (or never-mounted) layer +// namespace is not served by other active layers. { - const parent = nextMount(); - const child = path.join(parent, 'child'); const a = vfs.create(); const b = vfs.create(); - a.mount(parent); - assert.throws(() => b.mount(child), { code: 'ERR_INVALID_STATE' }); - a.unmount(); - - // Reverse direction: child first, then parent rejected - const c = vfs.create(); - const d = vfs.create(); - c.mount(child); - assert.throws(() => d.mount(parent), { code: 'ERR_INVALID_STATE' }); - c.unmount(); -} - -// Equal mount points: second one rejected -{ - const m = nextMount(); - const a = vfs.create(); - const b = vfs.create(); - a.mount(m); - assert.throws(() => b.mount(m), { code: 'ERR_INVALID_STATE' }); + a.writeFileSync('/f.txt', 'a'); + b.writeFileSync('/f.txt', 'b'); + const mountA = a.mount('/same'); + const mountB = b.mount('/same'); + b.unmount(); + // B's namespace is dead even though A is still mounted. + assert.strictEqual(fs.existsSync(path.join(mountB, 'f.txt')), false); + assert.strictEqual(fs.readFileSync(path.join(mountA, 'f.txt'), 'utf8'), 'a'); + // A malformed layer path under the VFS root is not served either. + const bogus = path.join(os.devNull, 'vfs', 'not-a-layer', 'f.txt'); + assert.strictEqual(fs.existsSync(bogus), false); a.unmount(); } // Double-mount of same instance rejected { const a = vfs.create(); - a.mount(nextMount()); - assert.throws(() => a.mount(nextMount()), { code: 'ERR_INVALID_STATE' }); + a.mount('/first'); + assert.throws(() => a.mount('/second'), { code: 'ERR_INVALID_STATE' }); a.unmount(); } diff --git a/test/parallel/test-vfs-mount.js b/test/parallel/test-vfs-mount.js index e59e39eef9713e..d78e312930b298 100644 --- a/test/parallel/test-vfs-mount.js +++ b/test/parallel/test-vfs-mount.js @@ -5,57 +5,87 @@ const common = require('../common'); const assert = require('assert'); const fs = require('fs'); const path = require('path'); +const os = require('os'); const vfs = require('node:vfs'); // Basic mount/unmount API and dispatch through node:vfs from the public fs. -const baseMountPoint = path.resolve('/tmp/vfs-mount-' + process.pid); -let mountCounter = 0; - function createMountedVfs() { - const mountPoint = baseMountPoint + '-' + (mountCounter++); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); - myVfs.mount(mountPoint); + const mountPoint = myVfs.mount('/data'); return { myVfs, mountPoint }; } -// Test: mounted/mountPoint getters +// Test: mounted/mountPoint getters; mount() returns the mount point in +// the reserved namespace under os.devNull. { const myVfs = vfs.create(); assert.strictEqual(myVfs.mounted, false); assert.strictEqual(myVfs.mountPoint, null); - const mountPoint = baseMountPoint + '-' + (mountCounter++); - myVfs.mount(mountPoint); + const mountPoint = myVfs.mount('/data'); assert.strictEqual(myVfs.mounted, true); assert.strictEqual(myVfs.mountPoint, mountPoint); + assert.strictEqual( + mountPoint, + path.join(os.devNull, 'vfs', `layer-${myVfs.layerId}`, 'data')); myVfs.unmount(); assert.strictEqual(myVfs.mounted, false); assert.strictEqual(myVfs.mountPoint, null); } +// Test: prefix is optional and never resolved against the working +// directory; relative and absolute prefixes are logical names inside +// the reserved namespace. +{ + const myVfs = vfs.create(); + const mountPoint = myVfs.mount(); + assert.strictEqual( + mountPoint, + path.join(os.devNull, 'vfs', `layer-${myVfs.layerId}`)); + myVfs.unmount(); + + const relative = myVfs.mount('deps'); + assert.strictEqual( + relative, + path.join(os.devNull, 'vfs', `layer-${myVfs.layerId}`, 'deps')); + myVfs.unmount(); +} + // Test: double-mount throws { const myVfs = vfs.create(); - const mountPoint = baseMountPoint + '-' + (mountCounter++); - myVfs.mount(mountPoint); - assert.throws(() => myVfs.mount(mountPoint), { code: 'ERR_INVALID_STATE' }); + myVfs.mount('/data'); + assert.throws(() => myVfs.mount('/data'), { code: 'ERR_INVALID_STATE' }); myVfs.unmount(); } -// Test: overlapping mounts throw +// Test: prefix cannot escape the reserved namespace; non-string throws +{ + const myVfs = vfs.create(); + assert.throws(() => myVfs.mount('../../escape'), + { code: 'ERR_INVALID_ARG_VALUE' }); + assert.throws(() => myVfs.mount(42), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.strictEqual(myVfs.mounted, false); +} + +// Test: two instances can use the same logical prefix - each gets a +// distinct mount point in its own layer namespace. { const a = vfs.create(); const b = vfs.create(); - const mountPoint = baseMountPoint + '-' + (mountCounter++); - a.mount(mountPoint); - assert.throws(() => b.mount(mountPoint), { code: 'ERR_INVALID_STATE' }); - assert.throws(() => b.mount(path.join(mountPoint, 'inner')), - { code: 'ERR_INVALID_STATE' }); + a.writeFileSync('/f.txt', 'A'); + b.writeFileSync('/f.txt', 'B'); + const mountA = a.mount('/same'); + const mountB = b.mount('/same'); + assert.notStrictEqual(mountA, mountB); + assert.strictEqual(fs.readFileSync(path.join(mountA, 'f.txt'), 'utf8'), 'A'); + assert.strictEqual(fs.readFileSync(path.join(mountB, 'f.txt'), 'utf8'), 'B'); a.unmount(); + b.unmount(); } // Test: fs.readFileSync intercepted @@ -165,8 +195,7 @@ function createMountedVfs() { // Test: Symbol.dispose unmounts { const myVfs = vfs.create(); - const mountPoint = baseMountPoint + '-' + (mountCounter++); - myVfs.mount(mountPoint); + myVfs.mount('/data'); assert.strictEqual(myVfs.mounted, true); myVfs[Symbol.dispose](); assert.strictEqual(myVfs.mounted, false); diff --git a/test/parallel/test-vfs-multi-mount.js b/test/parallel/test-vfs-multi-mount.js index 879f7cfcbf2b83..c52d11239b0a5b 100644 --- a/test/parallel/test-vfs-multi-mount.js +++ b/test/parallel/test-vfs-multi-mount.js @@ -1,8 +1,8 @@ // Flags: --experimental-vfs 'use strict'; -// Two concurrent non-overlapping mounts must each route to its own VFS without -// interference. Also exercises that the handler registry iterates and routes +// Two concurrent mounts must each route to its own VFS without +// interference. Also exercises that the layer registry routes // correctly when more than one VFS is active. require('../common'); @@ -11,9 +11,6 @@ const fs = require('fs'); const path = require('path'); const vfs = require('node:vfs'); -const baseA = path.resolve('/tmp/vfs-multi-a-' + process.pid); -const baseB = path.resolve('/tmp/vfs-multi-b-' + process.pid); - const a = vfs.create(); a.writeFileSync('/file.txt', 'from-a'); a.mkdirSync('/dir', { recursive: true }); @@ -24,8 +21,8 @@ b.writeFileSync('/file.txt', 'from-b'); b.mkdirSync('/dir', { recursive: true }); b.writeFileSync('/dir/inside.txt', 'b-inside'); -a.mount(baseA); -b.mount(baseB); +const baseA = a.mount('/multi'); +const baseB = b.mount('/multi'); // Each mount sees its own content assert.strictEqual(fs.readFileSync(path.join(baseA, 'file.txt'), 'utf8'), diff --git a/test/parallel/test-vfs-package-json-cache.js b/test/parallel/test-vfs-package-json-cache.js index 0c8c61e1facdc8..1d2dbc41cb1b13 100644 --- a/test/parallel/test-vfs-package-json-cache.js +++ b/test/parallel/test-vfs-package-json-cache.js @@ -12,12 +12,12 @@ const myVfs = vfs.create(); myVfs.mkdirSync('/pkg'); myVfs.writeFileSync('/pkg/package.json', '{"name":"test","type":"module"}'); myVfs.writeFileSync('/pkg/index.js', 'module.exports = 42'); -myVfs.mount('/mnt_pjcache'); +const mountPoint = myVfs.mount('/mnt_pjcache'); // Access the file so caches are populated -assert.ok(fs.existsSync('/mnt_pjcache/pkg/package.json')); +assert.ok(fs.existsSync(`${mountPoint}/pkg/package.json`)); // After unmount, cache should be cleared (no stale entries) myVfs.unmount(); -assert.strictEqual(fs.existsSync('/mnt_pjcache/pkg/package.json'), false); +assert.strictEqual(fs.existsSync(`${mountPoint}/pkg/package.json`), false); diff --git a/test/parallel/test-vfs-package-json.js b/test/parallel/test-vfs-package-json.js index c9ffe063e99d12..367fbad394f400 100644 --- a/test/parallel/test-vfs-package-json.js +++ b/test/parallel/test-vfs-package-json.js @@ -16,11 +16,11 @@ const vfs = require('node:vfs'); main: './index.js', exports: { '.': './index.js' }, })); - myVfs.mount('/mnt/read-test'); + const mountPoint = myVfs.mount('/mnt/read-test'); const packageJsonReader = require('internal/modules/package_json_reader'); const result = packageJsonReader.read( - path.resolve('/mnt/read-test/pkg/package.json'), {}, + path.join(mountPoint, 'pkg', 'package.json'), {}, ); assert.strictEqual(result.exists, true); @@ -30,12 +30,12 @@ const vfs = require('node:vfs'); assert.deepStrictEqual(result.exports, { '.': './index.js' }); assert.strictEqual( result.pjsonPath, - path.resolve('/mnt/read-test/pkg/package.json'), + path.join(mountPoint, 'pkg', 'package.json'), ); // Non-existent package.json returns exists: false const missing = packageJsonReader.read( - path.resolve('/mnt/read-test/nope/package.json'), {}, + path.join(mountPoint, 'nope', 'package.json'), {}, ); assert.strictEqual(missing.exists, false); @@ -51,11 +51,11 @@ const vfs = require('node:vfs'); type: 'module', })); myVfs.writeFileSync('/app/src/lib/deep/module.js', ''); - myVfs.mount('/mnt/parent-test'); + const mountPoint = myVfs.mount('/mnt/parent-test'); const packageJsonReader = require('internal/modules/package_json_reader'); const result = packageJsonReader.getNearestParentPackageJSON( - path.resolve('/mnt/parent-test/app/src/lib/deep/module.js'), + path.join(mountPoint, 'app', 'src', 'lib', 'deep', 'module.js'), ); assert.ok(result); @@ -64,7 +64,7 @@ const vfs = require('node:vfs'); assert.strictEqual(result.data.type, 'module'); assert.strictEqual( result.path, - path.resolve('/mnt/parent-test/app/package.json'), + path.join(mountPoint, 'app', 'package.json'), ); myVfs.unmount(); @@ -80,12 +80,12 @@ const vfs = require('node:vfs'); exports: { '.': './main.js' }, })); myVfs.writeFileSync('/project/src/index.js', ''); - myVfs.mount('/mnt/scope-test'); + const mountPoint = myVfs.mount('/mnt/scope-test'); const packageJsonReader = require('internal/modules/package_json_reader'); const { pathToFileURL } = require('url'); const scopeUrl = pathToFileURL( - path.resolve('/mnt/scope-test/project/src/index.js'), + path.join(mountPoint, 'project', 'src', 'index.js'), ).href; const result = packageJsonReader.getPackageScopeConfig(scopeUrl); @@ -94,17 +94,17 @@ const vfs = require('node:vfs'); assert.strictEqual(result.name, 'my-project'); assert.strictEqual( result.pjsonPath, - path.resolve('/mnt/scope-test/project/package.json'), + path.join(mountPoint, 'project', 'package.json'), ); // Path with no package.json returns exists: false const myVfs2 = vfs.create(); myVfs2.mkdirSync('/empty/src', { recursive: true }); myVfs2.writeFileSync('/empty/src/file.js', ''); - myVfs2.mount('/mnt/scope-empty'); + const mountPoint2 = myVfs2.mount('/mnt/scope-empty'); const emptyUrl = pathToFileURL( - path.resolve('/mnt/scope-empty/empty/src/file.js'), + path.join(mountPoint2, 'empty', 'src', 'file.js'), ).href; const emptyResult = packageJsonReader.getPackageScopeConfig(emptyUrl); assert.strictEqual(emptyResult.exists, false); @@ -121,12 +121,12 @@ const vfs = require('node:vfs'); type: 'module', })); myVfs.writeFileSync('/esm-app/index.js', ''); - myVfs.mount('/mnt/type-test'); + const mountPoint = myVfs.mount('/mnt/type-test'); const packageJsonReader = require('internal/modules/package_json_reader'); const { pathToFileURL } = require('url'); const typeUrl = pathToFileURL( - path.resolve('/mnt/type-test/esm-app/index.js'), + path.join(mountPoint, 'esm-app', 'index.js'), ).href; const type = packageJsonReader.getPackageType(typeUrl); assert.strictEqual(type, 'module'); @@ -135,10 +135,10 @@ const vfs = require('node:vfs'); const myVfs2 = vfs.create(); myVfs2.mkdirSync('/bare', { recursive: true }); myVfs2.writeFileSync('/bare/file.js', ''); - myVfs2.mount('/mnt/type-empty'); + const mountPoint2 = myVfs2.mount('/mnt/type-empty'); const noneUrl = pathToFileURL( - path.resolve('/mnt/type-empty/bare/file.js'), + path.join(mountPoint2, 'bare', 'file.js'), ).href; const noneType = packageJsonReader.getPackageType(noneUrl); assert.strictEqual(noneType, 'none'); @@ -157,9 +157,9 @@ const vfs = require('node:vfs'); })); myVfs.writeFileSync('/cjs-app/main.js', 'module.exports = { format: "cjs", ok: true };'); - myVfs.mount('/mnt/e2e-cjs'); + const mountPoint = myVfs.mount('/mnt/e2e-cjs'); - const result = require('/mnt/e2e-cjs/cjs-app/main.js'); + const result = require(`${mountPoint}/cjs-app/main.js`); assert.strictEqual(result.format, 'cjs'); assert.strictEqual(result.ok, true); @@ -174,10 +174,10 @@ const vfs = require('node:vfs'); type: 'module', })); myVfs.writeFileSync('/esm/mod.mjs', 'export const x = 42;'); - myVfs.mount('/mnt/e2e-esm'); + const mountPoint = myVfs.mount('/mnt/e2e-esm'); // Use .mjs to ensure ESM treatment regardless of package type - const mod = require('/mnt/e2e-esm/esm/mod.mjs'); + const mod = require(`${mountPoint}/esm/mod.mjs`); assert.strictEqual(mod.x, 42); myVfs.unmount(); diff --git a/test/parallel/test-vfs-require.js b/test/parallel/test-vfs-require.js index 8c6c49c73ada5d..6eaca0f0bfd059 100644 --- a/test/parallel/test-vfs-require.js +++ b/test/parallel/test-vfs-require.js @@ -9,14 +9,14 @@ const vfs = require('node:vfs'); // Test requiring a simple virtual module // VFS internal path: /hello.js -// Mount point: /virtual -// External path: /virtual/hello.js +// Logical prefix: /virtual — the actual mount point is returned by mount() +// External path: `${mountPoint}/hello.js` { const myVfs = vfs.create(); myVfs.writeFileSync('/hello.js', 'module.exports = "hello from vfs";'); - myVfs.mount('/virtual'); + const mountPoint = myVfs.mount('/virtual'); - const result = require('/virtual/hello.js'); + const result = require(`${mountPoint}/hello.js`); assert.strictEqual(result, 'hello from vfs'); myVfs.unmount(); @@ -32,9 +32,9 @@ const vfs = require('node:vfs'); getValue: function() { return 42; } }; `); - myVfs.mount('/virtual2'); + const mountPoint = myVfs.mount('/virtual2'); - const config = require('/virtual2/config.js'); + const config = require(`${mountPoint}/config.js`); assert.strictEqual(config.name, 'test-config'); assert.strictEqual(config.version, '1.0.0'); assert.strictEqual(config.getValue(), 42); @@ -42,23 +42,24 @@ const vfs = require('node:vfs'); myVfs.unmount(); } -// Test requiring a virtual module that requires another virtual module +// Test requiring a virtual module that requires another virtual module. +// Mount first so the embedded absolute specifier can use the mount point. { const myVfs = vfs.create(); - myVfs.writeFileSync('/utils.js', ` + const mountPoint = myVfs.mount('/virtual3'); + myVfs.writeFileSync(`${mountPoint}/utils.js`, ` module.exports = { add: function(a, b) { return a + b; } }; `); - myVfs.writeFileSync('/main.js', ` - const utils = require('/virtual3/utils.js'); + myVfs.writeFileSync(`${mountPoint}/main.js`, ` + const utils = require(${JSON.stringify(`${mountPoint}/utils.js`)}); module.exports = { sum: utils.add(10, 20) }; `); - myVfs.mount('/virtual3'); - const main = require('/virtual3/main.js'); + const main = require(`${mountPoint}/main.js`); assert.strictEqual(main.sum, 30); myVfs.unmount(); @@ -71,9 +72,9 @@ const vfs = require('node:vfs'); items: [1, 2, 3], enabled: true, })); - myVfs.mount('/virtual4'); + const mountPoint = myVfs.mount('/virtual4'); - const data = require('/virtual4/data.json'); + const data = require(`${mountPoint}/data.json`); assert.deepStrictEqual(data.items, [1, 2, 3]); assert.strictEqual(data.enabled, true); @@ -91,9 +92,9 @@ const vfs = require('node:vfs'); myVfs.writeFileSync('/my-package/index.js', ` module.exports = { loaded: true }; `); - myVfs.mount('/virtual5'); + const mountPoint = myVfs.mount('/virtual5'); - const pkg = require('/virtual5/my-package'); + const pkg = require(`${mountPoint}/my-package`); assert.strictEqual(pkg.loaded, true); myVfs.unmount(); @@ -126,9 +127,9 @@ const vfs = require('node:vfs'); const helper = require('./helper.js'); module.exports = helper.help(); `); - myVfs.mount('/virtual8'); + const mountPoint = myVfs.mount('/virtual8'); - const result = require('/virtual8/lib/index.js'); + const result = require(`${mountPoint}/lib/index.js`); assert.strictEqual(result, 'helped'); myVfs.unmount(); @@ -138,9 +139,9 @@ const vfs = require('node:vfs'); { const myVfs = vfs.create(); myVfs.writeFileSync('/file.txt', 'virtual content'); - myVfs.mount('/virtual9'); + const mountPoint = myVfs.mount('/virtual9'); - const content = fs.readFileSync('/virtual9/file.txt', 'utf8'); + const content = fs.readFileSync(`${mountPoint}/file.txt`, 'utf8'); assert.strictEqual(content, 'virtual content'); myVfs.unmount(); @@ -150,9 +151,9 @@ const vfs = require('node:vfs'); { const myVfs = vfs.create(); myVfs.writeFileSync('/esm.mjs', 'export const msg = "hello from esm";'); - myVfs.mount('/virtual11'); + const mountPoint = myVfs.mount('/virtual11'); - const mod = require('/virtual11/esm.mjs'); + const mod = require(`${mountPoint}/esm.mjs`); assert.strictEqual(mod.msg, 'hello from esm'); myVfs.unmount(); @@ -162,9 +163,9 @@ const vfs = require('node:vfs'); { const myVfs = vfs.create(); myVfs.writeFileSync('/esm-default.mjs', 'export default function() { return 42; }'); - myVfs.mount('/virtual12'); + const mountPoint = myVfs.mount('/virtual12'); - const mod = require('/virtual12/esm-default.mjs'); + const mod = require(`${mountPoint}/esm-default.mjs`); assert.strictEqual(mod.default(), 42); myVfs.unmount(); @@ -181,9 +182,9 @@ const vfs = require('node:vfs'); myVfs.writeFileSync('/app/lib.js', 'export const value = 42;' + ' export function hello() { return "hi"; }'); - myVfs.mount('/virtual13'); + const mountPoint = myVfs.mount('/virtual13'); - const mod = require('/virtual13/app/lib.js'); + const mod = require(`${mountPoint}/app/lib.js`); assert.strictEqual(mod.value, 42); assert.strictEqual(mod.hello(), 'hi'); @@ -201,9 +202,9 @@ const vfs = require('node:vfs'); myVfs.writeFileSync('/project/src/utils/math.js', 'export const add = (a, b) => a + b;' + ' export default 99;'); - myVfs.mount('/virtual14'); + const mountPoint = myVfs.mount('/virtual14'); - const mod = require('/virtual14/project/src/utils/math.js'); + const mod = require(`${mountPoint}/project/src/utils/math.js`); assert.strictEqual(mod.add(3, 4), 7); assert.strictEqual(mod.default, 99); @@ -219,9 +220,9 @@ const vfs = require('node:vfs'); })); myVfs.writeFileSync('/cjs-app/index.js', 'module.exports = { cjs: true };'); - myVfs.mount('/virtual15'); + const mountPoint = myVfs.mount('/virtual15'); - const mod = require('/virtual15/cjs-app/index.js'); + const mod = require(`${mountPoint}/cjs-app/index.js`); assert.strictEqual(mod.cjs, true); myVfs.unmount(); @@ -238,9 +239,9 @@ const vfs = require('node:vfs'); myVfs.writeFileSync('/multi/src/main.js', 'import { X } from "./dep.js";' + ' export const result = X + 1;'); - myVfs.mount('/virtual16'); + const mountPoint = myVfs.mount('/virtual16'); - const mod = require('/virtual16/multi/src/main.js'); + const mod = require(`${mountPoint}/multi/src/main.js`); assert.strictEqual(mod.result, 101); myVfs.unmount(); @@ -251,9 +252,9 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.writeFileSync('/no-pkg.mjs', 'export const x = 1; export default "hello";'); - myVfs.mount('/virtual17'); + const mountPoint = myVfs.mount('/virtual17'); - const mod = require('/virtual17/no-pkg.mjs'); + const mod = require(`${mountPoint}/no-pkg.mjs`); assert.strictEqual(mod.x, 1); assert.strictEqual(mod.default, 'hello'); @@ -267,9 +268,9 @@ const vfs = require('node:vfs'); myVfs.writeFileSync('/app/package.json', JSON.stringify({ name: 'no-type' })); myVfs.writeFileSync('/app/lib.mjs', 'export const val = 42;'); - myVfs.mount('/virtual18'); + const mountPoint = myVfs.mount('/virtual18'); - const mod = require('/virtual18/app/lib.mjs'); + const mod = require(`${mountPoint}/app/lib.mjs`); assert.strictEqual(mod.val, 42); myVfs.unmount(); @@ -284,9 +285,9 @@ const vfs = require('node:vfs'); type: 'commonjs', })); myVfs.writeFileSync('/cjs-pkg/esm.mjs', 'export const z = 99;'); - myVfs.mount('/virtual19'); + const mountPoint = myVfs.mount('/virtual19'); - const mod = require('/virtual19/cjs-pkg/esm.mjs'); + const mod = require(`${mountPoint}/cjs-pkg/esm.mjs`); assert.strictEqual(mod.z, 99); myVfs.unmount(); @@ -301,9 +302,9 @@ const vfs = require('node:vfs'); main: './lib/entry', })); myVfs.writeFileSync('/pkg/lib/entry.js', 'module.exports = "legacy-main";'); - myVfs.mount('/virtual20'); + const mountPoint = myVfs.mount('/virtual20'); - const result = require('/virtual20/pkg'); + const result = require(`${mountPoint}/pkg`); assert.strictEqual(result, 'legacy-main'); myVfs.unmount(); @@ -317,9 +318,9 @@ const vfs = require('node:vfs'); name: 'no-main-pkg', })); myVfs.writeFileSync('/pkg2/index.js', 'module.exports = "index-fallback";'); - myVfs.mount('/virtual21'); + const mountPoint = myVfs.mount('/virtual21'); - const result = require('/virtual21/pkg2'); + const result = require(`${mountPoint}/pkg2`); assert.strictEqual(result, 'index-fallback'); myVfs.unmount(); @@ -339,9 +340,9 @@ const vfs = require('node:vfs'); 'export const value = "esm-legacy-main";'); myVfs.writeFileSync('/app/main.mjs', 'export { value } from "esm-legacy-main";'); - myVfs.mount('/virtual20b'); + const mountPoint = myVfs.mount('/virtual20b'); - import('/virtual20b/app/main.mjs').then(common.mustCall((mod) => { + import(`${mountPoint}/app/main.mjs`).then(common.mustCall((mod) => { assert.strictEqual(mod.value, 'esm-legacy-main'); myVfs.unmount(); })); @@ -359,9 +360,9 @@ const vfs = require('node:vfs'); 'export const value = "esm-index-fallback";'); myVfs.writeFileSync('/app2/main.mjs', 'export { value } from "esm-nomain";'); - myVfs.mount('/virtual21b'); + const mountPoint = myVfs.mount('/virtual21b'); - import('/virtual21b/app2/main.mjs').then(common.mustCall((mod) => { + import(`${mountPoint}/app2/main.mjs`).then(common.mustCall((mod) => { assert.strictEqual(mod.value, 'esm-index-fallback'); myVfs.unmount(); })); @@ -376,10 +377,10 @@ const vfs = require('node:vfs'); type: 'module', })); myVfs.writeFileSync('/esm-pkg/entry', 'export const x = 123;'); - myVfs.mount('/virtual22'); + const mountPoint = myVfs.mount('/virtual22'); // Use import() to trigger ESM loader path for extensionless file detection - import('/virtual22/esm-pkg/entry').then(common.mustCall((mod) => { + import(`${mountPoint}/esm-pkg/entry`).then(common.mustCall((mod) => { assert.strictEqual(mod.x, 123); myVfs.unmount(); })); @@ -389,17 +390,17 @@ const vfs = require('node:vfs'); { const myVfs = vfs.create(); myVfs.writeFileSync('/unmount-test.js', 'module.exports = "before unmount";'); - myVfs.mount('/virtual10'); + const mountPoint = myVfs.mount('/virtual10'); - const result = require('/virtual10/unmount-test.js'); + const result = require(`${mountPoint}/unmount-test.js`); assert.strictEqual(result, 'before unmount'); myVfs.unmount(); // After unmounting, the file should not be found assert.throws(() => { - // Clear require cache first — the cache key is the platform-resolved path - delete require.cache[path.resolve('/virtual10/unmount-test.js')]; - require('/virtual10/unmount-test.js'); + // Clear require cache first — the cache key is the resolved mounted path + delete require.cache[path.join(mountPoint, 'unmount-test.js')]; + require(`${mountPoint}/unmount-test.js`); }, { code: 'MODULE_NOT_FOUND' }); } diff --git a/test/parallel/test-vfs-scoped-cache-purge.js b/test/parallel/test-vfs-scoped-cache-purge.js index 3b4147c59a5eca..e893eaad43afb5 100644 --- a/test/parallel/test-vfs-scoped-cache-purge.js +++ b/test/parallel/test-vfs-scoped-cache-purge.js @@ -2,12 +2,13 @@ 'use strict'; // Deregistering one of several mounted VFSes must scope-purge the -// loader caches: entries that belong to the VFS going away are -// dropped, entries from other VFSes are kept warm. ESM cache entries -// are tagged with `?vfs-layer=N` and surface in `import.meta.url`. +// loader caches: entries under the unmounted mount point are dropped, +// entries from other VFSes are kept warm. Module URLs are plain file +// URLs under the mount point - no synthetic search params. const common = require('../common'); const assert = require('assert'); +const { pathToFileURL } = require('node:url'); const vfs = require('node:vfs'); // 1) Multi-mount: deregister of one VFS keeps the other's CJS module @@ -15,17 +16,17 @@ const vfs = require('node:vfs'); { const a = vfs.create(); a.writeFileSync('/value.js', 'module.exports = "a-cached"'); - a.mount('/mnt-purge-a'); + const mountA = a.mount('/mnt-purge'); const b = vfs.create(); b.writeFileSync('/value.js', 'module.exports = "b-cached"'); - b.mount('/mnt-purge-b'); + const mountB = b.mount('/mnt-purge'); // Warm both caches. - assert.strictEqual(require('/mnt-purge-a/value.js'), 'a-cached'); - assert.strictEqual(require('/mnt-purge-b/value.js'), 'b-cached'); + assert.strictEqual(require(`${mountA}/value.js`), 'a-cached'); + assert.strictEqual(require(`${mountB}/value.js`), 'b-cached'); - const bKey = require.resolve('/mnt-purge-b/value.js'); + const bKey = require.resolve(`${mountB}/value.js`); assert.ok(bKey in require.cache, 'b should be cached before unmount'); // Unmount A. B's cache entry must survive. @@ -36,20 +37,28 @@ const vfs = require('node:vfs'); ); // Re-require yields the same module instance (identity preserved). - assert.strictEqual(require('/mnt-purge-b/value.js'), 'b-cached'); + assert.strictEqual(require(`${mountB}/value.js`), 'b-cached'); b.unmount(); } -// 2) ESM URLs are tagged with `vfs-layer=` and the tag surfaces in -// `import.meta.url`. +// 2) `import.meta.url` is the plain file URL of the module under the +// mount point, and repeated dynamic imports - including through +// `import.meta.resolve()` - return the same module namespace. (async () => { const v = vfs.create(); - v.writeFileSync('/m.mjs', 'export const url = import.meta.url;'); - v.mount('/mnt-tag'); + v.writeFileSync('/m.mjs', + 'export const url = import.meta.url;\n' + + 'export const resolved = import.meta.resolve("./m.mjs");'); + const mountPoint = v.mount('/mnt-url'); - const ns = await import('/mnt-tag/m.mjs'); - assert.match(ns.url, new RegExp(`vfs-layer=${v.layerId}`)); + const ns = await import(`${mountPoint}/m.mjs`); + assert.strictEqual(ns.url, pathToFileURL(`${mountPoint}/m.mjs`).href); + + // Stable identity: importing the URL the module sees for itself + // must hit the cache, not re-instantiate the module. + const nsAgain = await import(ns.resolved); + assert.strictEqual(ns, nsAgain); v.unmount(); })().then(common.mustCall()); From 722e10d2bf40f3e5724773e3e58fd4b9f7ce751c Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 7 Jul 2026 13:04:56 +0200 Subject: [PATCH 10/31] test: fix VFS module tests on Windows --- test/parallel/test-vfs-import.mjs | 19 ++++++++------ test/parallel/test-vfs-layer-tag-prefix.mjs | 9 ++++--- .../parallel/test-vfs-module-hooks-cleanup.js | 5 +++- test/parallel/test-vfs-module-hooks.mjs | 25 ++++++++++--------- test/parallel/test-vfs-require.js | 24 ++++++++++++++---- test/parallel/test-vfs-scoped-cache-purge.js | 4 ++- 6 files changed, 56 insertions(+), 30 deletions(-) diff --git a/test/parallel/test-vfs-import.mjs b/test/parallel/test-vfs-import.mjs index 21fd0d1a717d73..4455d4ad7fd371 100644 --- a/test/parallel/test-vfs-import.mjs +++ b/test/parallel/test-vfs-import.mjs @@ -1,8 +1,11 @@ // Flags: --experimental-vfs import '../common/index.mjs'; import assert from 'assert'; +import { pathToFileURL } from 'node:url'; import vfs from 'node:vfs'; +const vfsImport = (path) => pathToFileURL(path).href; + // NOTE: ESM imports are cached by URL and unmounting does not clear the V8 // module cache. Each vfs.create() gets its own layer id, so every mount // point returned by mount() is unique — no stale cache entries can be hit. @@ -13,7 +16,7 @@ import vfs from 'node:vfs'; myVfs.writeFileSync('/hello.mjs', 'export const message = "hello from vfs";'); const mountPoint = myVfs.mount('/esm-named'); - const { message } = await import(`${mountPoint}/hello.mjs`); + const { message } = await import(vfsImport(`${mountPoint}/hello.mjs`)); assert.strictEqual(message, 'hello from vfs'); myVfs.unmount(); @@ -25,7 +28,7 @@ import vfs from 'node:vfs'; myVfs.writeFileSync('/default.mjs', 'export default { name: "test", value: 42 };'); const mountPoint = myVfs.mount('/esm-default'); - const mod = await import(`${mountPoint}/default.mjs`); + const mod = await import(vfsImport(`${mountPoint}/default.mjs`)); assert.strictEqual(mod.default.name, 'test'); assert.strictEqual(mod.default.value, 42); @@ -40,11 +43,11 @@ import vfs from 'node:vfs'; myVfs.writeFileSync(`${mountPoint}/utils.mjs`, 'export function add(a, b) { return a + b; }'); myVfs.writeFileSync(`${mountPoint}/main.mjs`, ` - import { add } from ${JSON.stringify(`${mountPoint}/utils.mjs`)}; + import { add } from ${JSON.stringify(vfsImport(`${mountPoint}/utils.mjs`))}; export const result = add(10, 20); `); - const { result } = await import(`${mountPoint}/main.mjs`); + const { result } = await import(vfsImport(`${mountPoint}/main.mjs`)); assert.strictEqual(result, 30); myVfs.unmount(); @@ -61,7 +64,7 @@ import vfs from 'node:vfs'; `); const mountPoint = myVfs.mount('/esm-relative'); - const { output } = await import(`${mountPoint}/lib/index.mjs`); + const { output } = await import(vfsImport(`${mountPoint}/lib/index.mjs`)); assert.strictEqual(output, 'helped'); myVfs.unmount(); @@ -73,7 +76,7 @@ import vfs from 'node:vfs'; myVfs.writeFileSync('/data.json', JSON.stringify({ items: [1, 2, 3], enabled: true })); const mountPoint = myVfs.mount('/esm-json'); - const data = await import(`${mountPoint}/data.json`, { with: { type: 'json' } }); + const data = await import(vfsImport(`${mountPoint}/data.json`), { with: { type: 'json' } }); assert.deepStrictEqual(data.default.items, [1, 2, 3]); assert.strictEqual(data.default.enabled, true); @@ -100,7 +103,7 @@ import vfs from 'node:vfs'; myVfs.writeFileSync('/cjs-module.js', 'module.exports = { cjsValue: "cjs" };'); const mountPoint = myVfs.mount('/esm-mixed'); - const { esmValue } = await import(`${mountPoint}/esm-module.mjs`); + const { esmValue } = await import(vfsImport(`${mountPoint}/esm-module.mjs`)); assert.strictEqual(esmValue, 'esm'); // CJS require should also work (via createRequire) @@ -137,7 +140,7 @@ import vfs from 'node:vfs'; ); const mountPoint = myVfs.mount('/esm-bare'); - const { fromVfs } = await import(`${mountPoint}/app/entry.mjs`); + const { fromVfs } = await import(vfsImport(`${mountPoint}/app/entry.mjs`)); assert.strictEqual(fromVfs, true); myVfs.unmount(); diff --git a/test/parallel/test-vfs-layer-tag-prefix.mjs b/test/parallel/test-vfs-layer-tag-prefix.mjs index 3d2fe27dc83f9e..ee0274930d3fc1 100644 --- a/test/parallel/test-vfs-layer-tag-prefix.mjs +++ b/test/parallel/test-vfs-layer-tag-prefix.mjs @@ -7,8 +7,11 @@ import '../common/index.mjs'; import assert from 'node:assert'; +import { pathToFileURL } from 'node:url'; import vfs from 'node:vfs'; +const vfsImport = (path) => pathToFileURL(path).href; + // nextLayerId starts at 0 in a fresh process and increments per // VirtualFileSystem construction. Burn through constructions until we // have one VFS at layer 1 and one at layer 10 - the decimal-prefix @@ -35,8 +38,8 @@ layerTen.writeFileSync('/m.mjs', 'export const tag = "layer-ten";'); const mountTen = layerTen.mount('/mnt-tag'); // Warm both ESM cache entries. -const oneA = await import(`${mountOne}/m.mjs`); -const tenA = await import(`${mountTen}/m.mjs`); +const oneA = await import(vfsImport(`${mountOne}/m.mjs`)); +const tenA = await import(vfsImport(`${mountTen}/m.mjs`)); assert.strictEqual(oneA.tag, 'layer-one'); assert.strictEqual(tenA.tag, 'layer-ten'); @@ -49,7 +52,7 @@ layerOne.unmount(); // already-evaluated module namespace. A cache miss would cause the // loader to create a new module job and re-evaluate, producing a // different namespace object. -const tenB = await import(`${mountTen}/m.mjs`); +const tenB = await import(vfsImport(`${mountTen}/m.mjs`)); assert.strictEqual(tenA, tenB); layerTen.unmount(); diff --git a/test/parallel/test-vfs-module-hooks-cleanup.js b/test/parallel/test-vfs-module-hooks-cleanup.js index 1386a73f8c7f33..7b784d32ec0c00 100644 --- a/test/parallel/test-vfs-module-hooks-cleanup.js +++ b/test/parallel/test-vfs-module-hooks-cleanup.js @@ -7,8 +7,11 @@ const common = require('../common'); const assert = require('assert'); +const { pathToFileURL } = require('node:url'); const vfs = require('node:vfs'); +const vfsImport = (path) => pathToFileURL(path).href; + // 1) After a full deregister, a fresh register re-installs hooks cleanly: // the second mount must be visible to require(). { @@ -102,7 +105,7 @@ const vfs = require('node:vfs'); v.writeFileSync('/app/entry.mjs', "import 'badpkg';"); const mountPoint = v.mount('/mnt-legacy-err'); await assert.rejects( - () => import(`${mountPoint}/app/entry.mjs`), + () => import(vfsImport(`${mountPoint}/app/entry.mjs`)), (err) => { assert.strictEqual(err.code, 'ERR_MODULE_NOT_FOUND'); assert.match(err.message, /nope\.js/); diff --git a/test/parallel/test-vfs-module-hooks.mjs b/test/parallel/test-vfs-module-hooks.mjs index fc0bdabb014203..e215c2c48f176e 100644 --- a/test/parallel/test-vfs-module-hooks.mjs +++ b/test/parallel/test-vfs-module-hooks.mjs @@ -6,6 +6,7 @@ import { pathToFileURL } from 'node:url'; import vfs from 'node:vfs'; const require = createRequire(import.meta.url); +const vfsImport = (path) => pathToFileURL(path).href; // NOTE: mount() takes a purely logical prefix and returns the actual mount // point (a reserved path under os.devNull). Each test captures the @@ -77,7 +78,7 @@ const require = createRequire(import.meta.url); const mountPoint = myVfs.mount('/mh2'); // ESM import should get the 'import' condition - const esmResult = await import(`${mountPoint}/app/esm-entry.mjs`); + const esmResult = await import(vfsImport(`${mountPoint}/app/esm-entry.mjs`)); assert.strictEqual(esmResult.source, 'esm'); // CJS require should get the 'require' condition @@ -118,7 +119,7 @@ const require = createRequire(import.meta.url); ); const mountPoint = myVfs.mount('/mh3'); - const result = await import(`${mountPoint}/app/entry.mjs`); + const result = await import(vfsImport(`${mountPoint}/app/entry.mjs`)); assert.strictEqual(result.main, true); assert.strictEqual(result.feature, true); @@ -154,7 +155,7 @@ const require = createRequire(import.meta.url); ); const mountPoint = myVfs.mount('/mh4'); - const result = await import(`${mountPoint}/app/entry2.mjs`); + const result = await import(vfsImport(`${mountPoint}/app/entry2.mjs`)); assert.strictEqual(result.fromSubCond, 'esm'); myVfs.unmount(); @@ -194,7 +195,7 @@ const require = createRequire(import.meta.url); ); const mountPoint = myVfs.mount('/mh5'); - const result = await import(`${mountPoint}/app/entry3.mjs`); + const result = await import(vfsImport(`${mountPoint}/app/entry3.mjs`)); assert.strictEqual(result.nested, 'node-esm'); myVfs.unmount(); @@ -315,7 +316,7 @@ const require = createRequire(import.meta.url); ); const mountPoint = myVfs.mount('/mh11'); - const result = await import(`${mountPoint}/app/scoped-entry.mjs`); + const result = await import(vfsImport(`${mountPoint}/app/scoped-entry.mjs`)); assert.strictEqual(result.scoped, true); myVfs.unmount(); @@ -351,7 +352,7 @@ const require = createRequire(import.meta.url); ); const mountPoint = myVfs.mount('/mh12'); - const result = await import(`${mountPoint}/app/scoped-sub-entry.mjs`); + const result = await import(vfsImport(`${mountPoint}/app/scoped-sub-entry.mjs`)); assert.strictEqual(result.helpers, true); myVfs.unmount(); @@ -366,7 +367,7 @@ const require = createRequire(import.meta.url); myVfs.writeFileSync('/mod.js', 'export const fromModule = true;'); const mountPoint = myVfs.mount('/mh13'); - const result = await import(`${mountPoint}/mod.js`); + const result = await import(vfsImport(`${mountPoint}/mod.js`)); assert.strictEqual(result.fromModule, true); myVfs.unmount(); @@ -392,7 +393,7 @@ const require = createRequire(import.meta.url); `, ); - const result = await import(`${mountPoint}/use-cjs.js`); + const result = await import(vfsImport(`${mountPoint}/use-cjs.js`)); assert.strictEqual(result.cjsAlways, true); myVfs.unmount(); @@ -406,7 +407,7 @@ const require = createRequire(import.meta.url); myVfs.writeFileSync('/fileurl.mjs', 'export const fromFileUrl = true;'); const mountPoint = myVfs.mount('/mh15'); - const result = await import(pathToFileURL(`${mountPoint}/fileurl.mjs`).href); + const result = await import(vfsImport(`${mountPoint}/fileurl.mjs`)); assert.strictEqual(result.fromFileUrl, true); myVfs.unmount(); @@ -568,7 +569,7 @@ const require = createRequire(import.meta.url); const mountPoint = myVfs.mount('/mh23'); // 'browser' condition not active in Node, 'default' should match - const result = await import(`${mountPoint}/app/entry-default.mjs`); + const result = await import(vfsImport(`${mountPoint}/app/entry-default.mjs`)); assert.strictEqual(result.fromDefault, true); myVfs.unmount(); @@ -698,7 +699,7 @@ const require = createRequire(import.meta.url); `); const mountPoint = myVfs.mount('/mh31'); - const result = await import(`${mountPoint}/use-builtin.mjs`); + const result = await import(vfsImport(`${mountPoint}/use-builtin.mjs`)); assert.strictEqual(typeof result.sep, 'string'); myVfs.unmount(); @@ -712,7 +713,7 @@ const require = createRequire(import.meta.url); myVfs.writeFileSync('/data.json', JSON.stringify({ preformat: true })); const mountPoint = myVfs.mount('/mh32'); - const result = await import(`${mountPoint}/data.json`, { with: { type: 'json' } }); + const result = await import(vfsImport(`${mountPoint}/data.json`), { with: { type: 'json' } }); assert.strictEqual(result.default.preformat, true); myVfs.unmount(); diff --git a/test/parallel/test-vfs-require.js b/test/parallel/test-vfs-require.js index 6eaca0f0bfd059..cb5b8cc7e957a1 100644 --- a/test/parallel/test-vfs-require.js +++ b/test/parallel/test-vfs-require.js @@ -5,8 +5,11 @@ const common = require('../common'); const assert = require('assert'); const fs = require('fs'); const path = require('path'); +const { pathToFileURL } = require('node:url'); const vfs = require('node:vfs'); +const vfsImport = (path) => pathToFileURL(path).href; + // Test requiring a simple virtual module // VFS internal path: /hello.js // Logical prefix: /virtual — the actual mount point is returned by mount() @@ -342,7 +345,7 @@ const vfs = require('node:vfs'); 'export { value } from "esm-legacy-main";'); const mountPoint = myVfs.mount('/virtual20b'); - import(`${mountPoint}/app/main.mjs`).then(common.mustCall((mod) => { + import(vfsImport(`${mountPoint}/app/main.mjs`)).then(common.mustCall((mod) => { assert.strictEqual(mod.value, 'esm-legacy-main'); myVfs.unmount(); })); @@ -362,7 +365,7 @@ const vfs = require('node:vfs'); 'export { value } from "esm-nomain";'); const mountPoint = myVfs.mount('/virtual21b'); - import(`${mountPoint}/app2/main.mjs`).then(common.mustCall((mod) => { + import(vfsImport(`${mountPoint}/app2/main.mjs`)).then(common.mustCall((mod) => { assert.strictEqual(mod.value, 'esm-index-fallback'); myVfs.unmount(); })); @@ -380,7 +383,7 @@ const vfs = require('node:vfs'); const mountPoint = myVfs.mount('/virtual22'); // Use import() to trigger ESM loader path for extensionless file detection - import(`${mountPoint}/esm-pkg/entry`).then(common.mustCall((mod) => { + import(vfsImport(`${mountPoint}/esm-pkg/entry`)).then(common.mustCall((mod) => { assert.strictEqual(mod.x, 123); myVfs.unmount(); })); @@ -397,10 +400,21 @@ const vfs = require('node:vfs'); myVfs.unmount(); - // After unmounting, the file should not be found + // After unmounting, the VFS hooks should not serve the file. On Windows, + // the native loader may surface the NUL-backed former mount path as an + // invalid package config while probing for package.json. assert.throws(() => { // Clear require cache first — the cache key is the resolved mounted path delete require.cache[path.join(mountPoint, 'unmount-test.js')]; require(`${mountPoint}/unmount-test.js`); - }, { code: 'MODULE_NOT_FOUND' }); + }, (err) => { + if (common.isWindows) { + assert.ok( + ['ERR_INVALID_PACKAGE_CONFIG', 'MODULE_NOT_FOUND'].includes(err.code), + ); + } else { + assert.strictEqual(err.code, 'MODULE_NOT_FOUND'); + } + return true; + }); } diff --git a/test/parallel/test-vfs-scoped-cache-purge.js b/test/parallel/test-vfs-scoped-cache-purge.js index e893eaad43afb5..4627e532a148bc 100644 --- a/test/parallel/test-vfs-scoped-cache-purge.js +++ b/test/parallel/test-vfs-scoped-cache-purge.js @@ -11,6 +11,8 @@ const assert = require('assert'); const { pathToFileURL } = require('node:url'); const vfs = require('node:vfs'); +const vfsImport = (path) => pathToFileURL(path).href; + // 1) Multi-mount: deregister of one VFS keeps the other's CJS module // cache warm. The test relies on the require.cache being preserved. { @@ -52,7 +54,7 @@ const vfs = require('node:vfs'); 'export const resolved = import.meta.resolve("./m.mjs");'); const mountPoint = v.mount('/mnt-url'); - const ns = await import(`${mountPoint}/m.mjs`); + const ns = await import(vfsImport(`${mountPoint}/m.mjs`)); assert.strictEqual(ns.url, pathToFileURL(`${mountPoint}/m.mjs`).href); // Stable identity: importing the URL the module sees for itself From e7dd6548b16477f7869d9685d320ff5969514316 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 7 Jul 2026 13:20:48 +0200 Subject: [PATCH 11/31] doc: clarify VFS ESM imports on Windows --- doc/api/vfs.md | 32 +++++++++++++++++---- lib/internal/vfs/setup.js | 2 +- test/parallel/test-vfs-layer-tag-prefix.mjs | 2 +- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/doc/api/vfs.md b/doc/api/vfs.md index a38ccd73da902e..3d1216eb8f4501 100644 --- a/doc/api/vfs.md +++ b/doc/api/vfs.md @@ -354,12 +354,32 @@ console.log(greet()); // 'hi' myVfs.unmount(); ``` -Module identity follows the path: `__filename`, `module.filename`, -and `import.meta.url` are the plain absolute path (or `file:` URL) of -the module under the mount point, with no synthetic decorations. -Importing the same virtual path repeatedly — including through -`import.meta.resolve()` — yields the same module instance, exactly as -for real files. +For ECMAScript modules, convert mounted paths to `file:` URLs before +passing them to dynamic `import()`. This keeps VFS imports portable on +Windows, where mounted paths use Windows path syntax. + +```mjs +import { pathToFileURL } from 'node:url'; +import vfs from 'node:vfs'; + +const myVfs = vfs.create(); +myVfs.writeFileSync('/mod.mjs', 'export const value = 42;'); +const mountPoint = myVfs.mount('/virtual'); + +const { value } = await import( + pathToFileURL(`${mountPoint}/mod.mjs`).href, +); +console.log(value); // 42 + +myVfs.unmount(); +``` + +Module identity follows the path: `__filename` and `module.filename` +are the plain absolute path of the module under the mount point, and +`import.meta.url` is the corresponding `file:` URL, with no synthetic +decorations. Importing the same virtual path repeatedly — including +through `import.meta.resolve()` — yields the same module instance, +exactly as for real files. Calling [`vfs.unmount()`][] invalidates the modules that were loaded from the mount point: a subsequent `require()` or `import` of a path diff --git a/lib/internal/vfs/setup.js b/lib/internal/vfs/setup.js index a14ac93a2ac674..47981befe0bae0 100644 --- a/lib/internal/vfs/setup.js +++ b/lib/internal/vfs/setup.js @@ -87,7 +87,7 @@ function writeFileSyncFd(fd, data, options) { } // Registry of active VFS instances, keyed by layer id. Because every -// mount point embeds its layer id (`${execPath}/vfs/layer-/...`), +// mount point embeds its layer id (`${os.devNull}/vfs/layer-/...`), // dispatch is a map lookup rather than a scan of mounted layers. const activeVFSLayers = new SafeMap(); diff --git a/test/parallel/test-vfs-layer-tag-prefix.mjs b/test/parallel/test-vfs-layer-tag-prefix.mjs index ee0274930d3fc1..8f21c82149f7d0 100644 --- a/test/parallel/test-vfs-layer-tag-prefix.mjs +++ b/test/parallel/test-vfs-layer-tag-prefix.mjs @@ -1,7 +1,7 @@ // Flags: --experimental-vfs // Layer identity is carried by the mount-point path itself -// (`${execPath}/vfs/layer-/...`), so unmounting one layer must +// (`${os.devNull}/vfs/layer-/...`), so unmounting one layer must // purge exactly that layer's ESM cache entries - layers whose ids // share a decimal prefix (1 vs 10) must be unaffected. From de17e814a1cf099be355a2a59e144a69d1421fff Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 7 Jul 2026 13:21:17 +0200 Subject: [PATCH 12/31] doc: remove em dashes from VFS docs --- doc/api/vfs.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/api/vfs.md b/doc/api/vfs.md index 3d1216eb8f4501..b8ac7bfc630218 100644 --- a/doc/api/vfs.md +++ b/doc/api/vfs.md @@ -125,7 +125,7 @@ added: REPLACEME Mounts the virtual file system and returns the resulting mount point. After mounting, files in the VFS can be accessed through the -`node:fs` module — and resolved through `require()` and `import` — +`node:fs` module and resolved through `require()` and `import` using paths under the returned mount point. Mount points always live inside a reserved namespace, @@ -135,7 +135,7 @@ cannot have child file system entries, no real file-system path can exist under this namespace: virtual paths never conflate with (or shadow) real paths, and the layer that owns a path is visible in the path itself. The `prefix` argument is a purely logical name inside -the namespace — it is never resolved against the working directory, +the namespace. It is never resolved against the working directory, and a prefix that attempts to escape the namespace (for example with `..` segments) throws `ERR_INVALID_ARG_VALUE`. @@ -377,8 +377,8 @@ myVfs.unmount(); Module identity follows the path: `__filename` and `module.filename` are the plain absolute path of the module under the mount point, and `import.meta.url` is the corresponding `file:` URL, with no synthetic -decorations. Importing the same virtual path repeatedly — including -through `import.meta.resolve()` — yields the same module instance, +decorations. Importing the same virtual path repeatedly, including +through `import.meta.resolve()`, yields the same module instance, exactly as for real files. Calling [`vfs.unmount()`][] invalidates the modules that were loaded From a17ee2980c41dc03326ffdcf516d55cb5c48ec58 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 7 Jul 2026 16:21:31 +0200 Subject: [PATCH 13/31] vfs: drop mount() prefix argument and layer- path segment `mount()` no longer accepts an argument. The user-supplied prefix served no purpose beyond a cosmetic label - actual layer identity comes from the per-instance `layerId`, so distinct instances never shared a mount point even when they passed the same prefix. Removing the argument eliminates the input validation, the escape check for `..` segments, and the API-shape doubt about what a "logical prefix" means. The `layer-` prefix on the layer segment of the mount path is likewise gone: mount points are now `${os.devNull}/vfs/` (for example `/dev/null/vfs/0`) instead of `${os.devNull}/vfs/layer-0/`. The parser in `getLayerIdFromPath` simplifies to reading the digit segment immediately after the VFS root. Docs, tests and benchmark adapted. The dispatch benchmark still reports flat per-call latency across 1..10 mounted layers. Signed-off-by: Matteo Collina --- benchmark/vfs/bench-fs-dispatch.js | 2 +- doc/api/vfs.md | 37 +++++------- lib/internal/vfs/file_system.js | 40 ++++--------- lib/internal/vfs/router.js | 24 ++++---- lib/internal/vfs/setup.js | 4 +- test/parallel/test-vfs-destructuring.js | 6 +- test/parallel/test-vfs-fs-accessSync.js | 2 +- .../test-vfs-fs-callback-error-paths.js | 2 +- test/parallel/test-vfs-fs-chmod-callback.js | 2 +- test/parallel/test-vfs-fs-chmodSync.js | 2 +- test/parallel/test-vfs-fs-copyFileSync.js | 2 +- test/parallel/test-vfs-fs-createReadStream.js | 2 +- .../parallel/test-vfs-fs-createWriteStream.js | 2 +- test/parallel/test-vfs-fs-existsSync.js | 2 +- test/parallel/test-vfs-fs-fchmod-callback.js | 2 +- test/parallel/test-vfs-fs-linkSync.js | 2 +- test/parallel/test-vfs-fs-mkdir-callback.js | 2 +- test/parallel/test-vfs-fs-mkdirSync.js | 2 +- test/parallel/test-vfs-fs-mkdtempSync.js | 2 +- test/parallel/test-vfs-fs-open-callback.js | 2 +- test/parallel/test-vfs-fs-openAsBlob.js | 2 +- test/parallel/test-vfs-fs-openSync.js | 2 +- test/parallel/test-vfs-fs-opendir-callback.js | 2 +- test/parallel/test-vfs-fs-opendirSync.js | 2 +- .../test-vfs-fs-promises-buffer-encoding.js | 2 +- .../test-vfs-fs-promises-stat-no-throw.js | 2 +- test/parallel/test-vfs-fs-promises.js | 2 +- .../parallel/test-vfs-fs-readFile-callback.js | 2 +- test/parallel/test-vfs-fs-readFileSync.js | 4 +- test/parallel/test-vfs-fs-readdirSync.js | 2 +- test/parallel/test-vfs-fs-realpathSync.js | 2 +- test/parallel/test-vfs-fs-rename-callback.js | 2 +- test/parallel/test-vfs-fs-renameSync.js | 2 +- test/parallel/test-vfs-fs-rmSync.js | 2 +- test/parallel/test-vfs-fs-stat-callback.js | 2 +- test/parallel/test-vfs-fs-statSync.js | 2 +- test/parallel/test-vfs-fs-symlink-callback.js | 2 +- test/parallel/test-vfs-fs-symlinkSync.js | 2 +- .../parallel/test-vfs-fs-truncate-callback.js | 2 +- test/parallel/test-vfs-fs-truncateSync.js | 2 +- test/parallel/test-vfs-fs-watch-dispatch.js | 2 +- .../test-vfs-fs-writeFile-callback.js | 2 +- test/parallel/test-vfs-fs-writeFileSync.js | 4 +- test/parallel/test-vfs-import.mjs | 16 ++--- .../parallel/test-vfs-invalid-package-json.js | 6 +- test/parallel/test-vfs-layer-id.js | 6 +- test/parallel/test-vfs-layer-tag-prefix.mjs | 18 +++--- .../parallel/test-vfs-module-hooks-cleanup.js | 20 +++---- test/parallel/test-vfs-module-hooks.mjs | 58 +++++++++---------- test/parallel/test-vfs-mount-errors.js | 35 +++++------ test/parallel/test-vfs-mount.js | 53 +++++------------ test/parallel/test-vfs-multi-mount.js | 4 +- test/parallel/test-vfs-package-json-cache.js | 2 +- test/parallel/test-vfs-package-json.js | 16 ++--- test/parallel/test-vfs-require.js | 46 +++++++-------- test/parallel/test-vfs-scoped-cache-purge.js | 6 +- 56 files changed, 215 insertions(+), 262 deletions(-) diff --git a/benchmark/vfs/bench-fs-dispatch.js b/benchmark/vfs/bench-fs-dispatch.js index 9ae681669f83ac..866fa59bc876ce 100644 --- a/benchmark/vfs/bench-fs-dispatch.js +++ b/benchmark/vfs/bench-fs-dispatch.js @@ -35,7 +35,7 @@ function mountLayers(count) { const handles = []; for (let i = 0; i < count; i++) { const v = vfs.create(); - v.mount('/bench'); + v.mount(); handles.push(v); } return handles; diff --git a/doc/api/vfs.md b/doc/api/vfs.md index b8ac7bfc630218..9771ea9df4b49b 100644 --- a/doc/api/vfs.md +++ b/doc/api/vfs.md @@ -63,7 +63,7 @@ shape of the [`node:fs`][] API. All paths are POSIX-style and absolute By default, the file tree is private to the VFS instance. To expose it through the global `node:fs` module, `require()`, and `import`, -call [`vfs.mount(prefix)`][]; call [`vfs.unmount()`][] (or rely on a +call [`vfs.mount()`][]; call [`vfs.unmount()`][] (or rely on a `using` declaration) to detach again. ## `vfs.create([provider][, options])` @@ -112,15 +112,12 @@ added: v26.4.0 * `emitExperimentalWarning` {boolean} Whether to emit the experimental warning. **Default:** `true`. -### `vfs.mount([prefix])` +### `vfs.mount()` -* `prefix` {string} A logical name for the mount inside the reserved - VFS namespace. Interpreted as a relative path; leading separators - are ignored. **Default:** `'/'`. * Returns: {string} The absolute mount point. Mounts the virtual file system and returns the resulting mount point. @@ -129,15 +126,12 @@ After mounting, files in the VFS can be accessed through the using paths under the returned mount point. Mount points always live inside a reserved namespace, -`${os.devNull}/vfs/layer-/`. Because [`os.devNull`][] is a +`${os.devNull}/vfs/`. Because [`os.devNull`][] is a character device (POSIX) or a device-namespace path (Windows) that cannot have child file system entries, no real file-system path can exist under this namespace: virtual paths never conflate with (or shadow) real paths, and the layer that owns a path is visible in the -path itself. The `prefix` argument is a purely logical name inside -the namespace. It is never resolved against the working directory, -and a prefix that attempts to escape the namespace (for example with -`..` segments) throws `ERR_INVALID_ARG_VALUE`. +path itself. ```cjs const vfs = require('node:vfs'); @@ -145,8 +139,8 @@ const fs = require('node:fs'); const myVfs = vfs.create(); myVfs.writeFileSync('/data.txt', 'Hello'); -const mountPoint = myVfs.mount('/virtual'); -// e.g. '/dev/null/vfs/layer-0/virtual' +const mountPoint = myVfs.mount(); +// e.g. '/dev/null/vfs/0' fs.readFileSync(`${mountPoint}/data.txt`, 'utf8'); // 'Hello' ``` @@ -154,8 +148,8 @@ fs.readFileSync(`${mountPoint}/data.txt`, 'utf8'); // 'Hello' Each `VirtualFileSystem` instance may be mounted at most once at a time. Attempting to mount an already-mounted instance throws `ERR_INVALID_STATE`. Because each instance mounts inside its own -`layer-` namespace, mounts from different instances can -never overlap, even when they use the same `prefix`. +per-layer namespace, mounts from different instances can never +overlap. The VFS supports the [Explicit Resource Management][] proposal. Use a `using` declaration to unmount automatically when leaving scope: @@ -168,7 +162,7 @@ let mountPoint; { using myVfs = vfs.create(); myVfs.writeFileSync('/data.txt', 'Hello'); - mountPoint = myVfs.mount('/virtual'); + mountPoint = myVfs.mount(); fs.readFileSync(`${mountPoint}/data.txt`, 'utf8'); // 'Hello' } // VFS is automatically unmounted here @@ -184,8 +178,7 @@ added: REPLACEME Unmounts the virtual file system. After unmounting, virtual files are no longer reachable through `node:fs`, `require()`, or `import`. -The same instance may be mounted again, at the same or a different -prefix, by calling `mount()`. +The same instance may be mounted again by calling `mount()`. This method is idempotent: calling `unmount()` on a VFS that is not currently mounted has no effect. @@ -209,7 +202,7 @@ added: REPLACEME * {string | null} The current mount point as an absolute string (the value returned by -the last [`vfs.mount(prefix)`][] call), or `null` when the VFS is not +the last [`vfs.mount()`][] call), or `null` when the VFS is not mounted. ### `vfs.layerId` @@ -225,7 +218,7 @@ construction. The id is stable across `mount()` / `unmount()` cycles for the lifetime of the instance, and is independent of the order in which VFS layers are mounted. -The layer id forms the `layer-` segment of the reserved mount +The layer id forms the `` segment of the reserved mount namespace, so every path served by this instance carries the id, and it appears in the `NODE_DEBUG=vfs` output for `register` and `deregister` events. @@ -346,7 +339,7 @@ myVfs.mkdirSync('/lib'); myVfs.writeFileSync('/lib/greet.js', 'module.exports = () => "hi";'); myVfs.writeFileSync( '/lib/package.json', '{"main": "./greet.js"}'); -const mountPoint = myVfs.mount('/virtual'); +const mountPoint = myVfs.mount(); const greet = require(`${mountPoint}/lib`); console.log(greet()); // 'hi' @@ -364,7 +357,7 @@ import vfs from 'node:vfs'; const myVfs = vfs.create(); myVfs.writeFileSync('/mod.mjs', 'export const value = 42;'); -const mountPoint = myVfs.mount('/virtual'); +const mountPoint = myVfs.mount(); const { value } = await import( pathToFileURL(`${mountPoint}/mod.mjs`).href, @@ -524,5 +517,5 @@ fields use synthetic but stable values: [`fs.Stats`]: fs.md#class-fsstats [`node:fs`]: fs.md [`os.devNull`]: os.md#osdevnull -[`vfs.mount(prefix)`]: #vfsmountprefix +[`vfs.mount()`]: #vfsmount [`vfs.unmount()`]: #vfsunmount diff --git a/lib/internal/vfs/file_system.js b/lib/internal/vfs/file_system.js index 6846e55881d519..766a7ea00cdbbd 100644 --- a/lib/internal/vfs/file_system.js +++ b/lib/internal/vfs/file_system.js @@ -9,14 +9,13 @@ const { const { codes: { - ERR_INVALID_ARG_VALUE, ERR_INVALID_STATE, }, } = require('internal/errors'); -const { validateBoolean, validateString } = require('internal/validators'); +const { validateBoolean } = require('internal/validators'); const { MemoryProvider } = require('internal/vfs/providers/memory'); const path = require('path'); -const { posix: pathPosix, join: platformJoin, resolve: resolvePath, toNamespacedPath } = path; +const { posix: pathPosix, resolve: resolvePath, toNamespacedPath } = path; const { join: joinPath } = pathPosix; const { getLayerRoot, @@ -169,37 +168,22 @@ class VirtualFileSystem { // ==================== Mount ==================== /** - * Mounts the VFS inside this instance's reserved namespace and - * returns the resulting mount point. The mount point always lives - * under `${os.devNull}/vfs/layer-/` - a location that - * cannot exist on the real file system (`os.devNull` is a - * character device on POSIX and a device-namespace path on Windows; - * neither can have child filesystem entries) - so virtual paths - * never conflate with real paths and the owning layer is decidable - * from the path alone. - * @param {string} [prefix] Logical prefix appended to the reserved - * namespace. Interpreted as a relative path inside the namespace; - * leading separators are ignored. + * Mounts the VFS at its reserved mount point and returns that + * mount point. The mount point is `${os.devNull}/vfs/` - + * a location that cannot exist on the real file system (`os.devNull` + * is a character device on POSIX and a device-namespace path on + * Windows; neither can have child filesystem entries) - so virtual + * paths never conflate with real paths, and the owning layer of any + * path is decidable from the path alone. * @returns {string} The absolute mount point */ - mount(prefix = '/') { + mount() { if (this[kMounted]) { throw new ERR_INVALID_STATE('VFS is already mounted'); } - validateString(prefix, 'prefix'); - const layerRoot = getLayerRoot(this[kLayerId]); - // platformJoin treats an absolute prefix as relative to layerRoot; - // resolvePath drops any trailing separator (e.g. for prefix '/'). - // The containment check rejects '..' segments escaping the - // reserved namespace. - const mountPoint = resolvePath(platformJoin(layerRoot, prefix)); - const normalized = normalizeMountedPath(mountPoint); - if (!isUnderMountPoint(normalized, normalizeMountedPath(layerRoot))) { - throw new ERR_INVALID_ARG_VALUE( - 'prefix', prefix, 'must not escape the VFS namespace'); - } + const mountPoint = getLayerRoot(this[kLayerId]); this[kMountPoint] = mountPoint; - this[kNormalizedMountPoint] = normalized; + this[kNormalizedMountPoint] = normalizeMountedPath(mountPoint); this[kMounted] = true; debug('mount %s', mountPoint); loadVfsSetup(); diff --git a/lib/internal/vfs/router.js b/lib/internal/vfs/router.js index 3e0780d20b4ec8..0a47836f69021f 100644 --- a/lib/internal/vfs/router.js +++ b/lib/internal/vfs/router.js @@ -9,7 +9,7 @@ const { const { isAbsolute, relative, resolve, sep, toNamespacedPath } = require('path'); -// All VFS mount points live under `${os.devNull}/vfs/layer-/`. +// All VFS mount points live under `${os.devNull}/vfs//`. // `os.devNull` is a character device on POSIX (`/dev/null`) and a // device-namespace path on Windows (`\\.\NUL`); neither can have child // filesystem entries, so no real file-system path can exist under this @@ -32,30 +32,26 @@ function getNormalizedVfsRoot() { } /** - * Returns the reserved namespace root for a VFS layer: - * `${os.devNull}/vfs/layer-`. + * Returns the reserved mount point for a VFS layer: + * `${os.devNull}/vfs/`. * @param {number} layerId * @returns {string} */ function getLayerRoot(layerId) { - return getVfsRoot() + sep + 'layer-' + layerId; + return getVfsRoot() + sep + layerId; } /** * Extracts the layer id from a normalized path under the VFS root, or - * returns -1 when the path does not carry a well-formed - * `.../vfs/layer-` segment. The caller must have verified that - * `normalizedPath` starts with the normalized VFS root. + * returns -1 when the path does not carry a well-formed `.../vfs/` + * segment. The caller must have verified that `normalizedPath` starts + * with the normalized VFS root plus a separator. * @param {string} normalizedPath * @returns {number} */ function getLayerIdFromPath(normalizedPath) { - // Skip 'layer-'. - const start = getNormalizedVfsRoot().length + 7; - if (!StringPrototypeStartsWith(normalizedPath, sep + 'layer-', - getNormalizedVfsRoot().length)) { - return -1; - } + // Skip ''. + const start = getNormalizedVfsRoot().length + 1; let id = 0; let i = start; const len = normalizedPath.length; @@ -66,7 +62,7 @@ function getLayerIdFromPath(normalizedPath) { if (c < 48 || c > 57) return -1; // not a digit id = id * 10 + (c - 48); } - if (i === start) return -1; // 'layer-' with no digits + if (i === start) return -1; // no digits at all return id; } diff --git a/lib/internal/vfs/setup.js b/lib/internal/vfs/setup.js index 47981befe0bae0..dc696d96c0db2e 100644 --- a/lib/internal/vfs/setup.js +++ b/lib/internal/vfs/setup.js @@ -87,7 +87,7 @@ function writeFileSyncFd(fd, data, options) { } // Registry of active VFS instances, keyed by layer id. Because every -// mount point embeds its layer id (`${os.devNull}/vfs/layer-/...`), +// mount point embeds its layer id (`${os.devNull}/vfs/`), // dispatch is a map lookup rather than a scan of mounted layers. const activeVFSLayers = new SafeMap(); @@ -128,7 +128,7 @@ function deregisterVFS(vfs) { /** * Resolves a path string to the active VFS that owns it, or null. * Ownership is decidable from the path alone: all mount points live - * under the reserved `${os.devNull}/vfs/layer-/` namespace, + * under the reserved `${os.devNull}/vfs/` namespace, * so a single prefix comparison rejects every real-file-system path * and a map lookup finds the owning layer. * @param {string} inputPath diff --git a/test/parallel/test-vfs-destructuring.js b/test/parallel/test-vfs-destructuring.js index 5c2d074cc74615..14d6f51234c495 100644 --- a/test/parallel/test-vfs-destructuring.js +++ b/test/parallel/test-vfs-destructuring.js @@ -24,9 +24,9 @@ myVfs.writeFileSync('/file.txt', 'hello from vfs'); myVfs.writeFileSync('/sub/nested.txt', 'nested content'); // mount() returns the actual mount point (under os.devNull/vfs/...); -// '/vfs_destr' is just a logical label. The returned path is already in the -// platform's native form, so assertion targets are built from it directly. -const MOUNT = myVfs.mount('/vfs_destr'); +// the returned path is already in the platform's native form, so assertion +// targets are built from it directly. +const MOUNT = myVfs.mount(); const FILE = path.join(MOUNT, 'file.txt'); { diff --git a/test/parallel/test-vfs-fs-accessSync.js b/test/parallel/test-vfs-fs-accessSync.js index cc2794a5ad7e5a..9d3469608ecc39 100644 --- a/test/parallel/test-vfs-fs-accessSync.js +++ b/test/parallel/test-vfs-fs-accessSync.js @@ -12,7 +12,7 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); -const mountPoint = myVfs.mount('/accessSync'); +const mountPoint = myVfs.mount(); // Existing path succeeds fs.accessSync(path.join(mountPoint, 'src/hello.txt')); diff --git a/test/parallel/test-vfs-fs-callback-error-paths.js b/test/parallel/test-vfs-fs-callback-error-paths.js index be7506b297ce96..5cff379eda668a 100644 --- a/test/parallel/test-vfs-fs-callback-error-paths.js +++ b/test/parallel/test-vfs-fs-callback-error-paths.js @@ -13,7 +13,7 @@ function mounted() { const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); - const mountPoint = myVfs.mount('/m'); + const mountPoint = myVfs.mount(); return { myVfs, mountPoint }; } diff --git a/test/parallel/test-vfs-fs-chmod-callback.js b/test/parallel/test-vfs-fs-chmod-callback.js index 28c6bb4bc5c01d..44f86833c397b0 100644 --- a/test/parallel/test-vfs-fs-chmod-callback.js +++ b/test/parallel/test-vfs-fs-chmod-callback.js @@ -12,7 +12,7 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); -const mountPoint = myVfs.mount('/chmod-cb'); +const mountPoint = myVfs.mount(); const target = path.join(mountPoint, 'src/hello.txt'); const uid = process.getuid?.() ?? 0; diff --git a/test/parallel/test-vfs-fs-chmodSync.js b/test/parallel/test-vfs-fs-chmodSync.js index 0862b7bd3438e3..91a63c36129eb3 100644 --- a/test/parallel/test-vfs-fs-chmodSync.js +++ b/test/parallel/test-vfs-fs-chmodSync.js @@ -13,7 +13,7 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); -const mountPoint = myVfs.mount('/chmodSync'); +const mountPoint = myVfs.mount(); const target = path.join(mountPoint, 'src/hello.txt'); const uid = process.getuid?.() ?? 0; diff --git a/test/parallel/test-vfs-fs-copyFileSync.js b/test/parallel/test-vfs-fs-copyFileSync.js index d9b2a9aa81b990..3383b460862caf 100644 --- a/test/parallel/test-vfs-fs-copyFileSync.js +++ b/test/parallel/test-vfs-fs-copyFileSync.js @@ -12,7 +12,7 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); -const mountPoint = myVfs.mount('/copyFileSync'); +const mountPoint = myVfs.mount(); fs.copyFileSync( path.join(mountPoint, 'src/hello.txt'), diff --git a/test/parallel/test-vfs-fs-createReadStream.js b/test/parallel/test-vfs-fs-createReadStream.js index 76b710ee3e5e4b..357d7db2b92327 100644 --- a/test/parallel/test-vfs-fs-createReadStream.js +++ b/test/parallel/test-vfs-fs-createReadStream.js @@ -14,7 +14,7 @@ function mounted() { const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); - const mountPoint = myVfs.mount('/m'); + const mountPoint = myVfs.mount(); return { myVfs, mountPoint }; } diff --git a/test/parallel/test-vfs-fs-createWriteStream.js b/test/parallel/test-vfs-fs-createWriteStream.js index 1d9cf20c07df88..495d4108db3723 100644 --- a/test/parallel/test-vfs-fs-createWriteStream.js +++ b/test/parallel/test-vfs-fs-createWriteStream.js @@ -13,7 +13,7 @@ const vfs = require('node:vfs'); function mounted() { const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); - const mountPoint = myVfs.mount('/m'); + const mountPoint = myVfs.mount(); return { myVfs, mountPoint }; } diff --git a/test/parallel/test-vfs-fs-existsSync.js b/test/parallel/test-vfs-fs-existsSync.js index f351cf7b1fcc49..5bb6df4c1c2029 100644 --- a/test/parallel/test-vfs-fs-existsSync.js +++ b/test/parallel/test-vfs-fs-existsSync.js @@ -12,7 +12,7 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); -const mountPoint = myVfs.mount('/existsSync'); +const mountPoint = myVfs.mount(); assert.strictEqual(fs.existsSync(path.join(mountPoint, 'src/hello.txt')), true); assert.strictEqual(fs.existsSync(path.join(mountPoint, 'src')), true); diff --git a/test/parallel/test-vfs-fs-fchmod-callback.js b/test/parallel/test-vfs-fs-fchmod-callback.js index 9fed2d6cd040ab..e46fd27af4249b 100644 --- a/test/parallel/test-vfs-fs-fchmod-callback.js +++ b/test/parallel/test-vfs-fs-fchmod-callback.js @@ -12,7 +12,7 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); -const mountPoint = myVfs.mount('/fchmod-cb'); +const mountPoint = myVfs.mount(); const fd = fs.openSync(path.join(mountPoint, 'src/hello.txt'), 'r+'); const uid = process.getuid?.() ?? 0; diff --git a/test/parallel/test-vfs-fs-linkSync.js b/test/parallel/test-vfs-fs-linkSync.js index 003a162b99bfdc..e73bcc76df006a 100644 --- a/test/parallel/test-vfs-fs-linkSync.js +++ b/test/parallel/test-vfs-fs-linkSync.js @@ -12,7 +12,7 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); -const mountPoint = myVfs.mount('/linkSync'); +const mountPoint = myVfs.mount(); fs.linkSync( path.join(mountPoint, 'src/hello.txt'), diff --git a/test/parallel/test-vfs-fs-mkdir-callback.js b/test/parallel/test-vfs-fs-mkdir-callback.js index 000a60cf002c1b..4caccda70f6d25 100644 --- a/test/parallel/test-vfs-fs-mkdir-callback.js +++ b/test/parallel/test-vfs-fs-mkdir-callback.js @@ -13,7 +13,7 @@ function mounted() { const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); - const mountPoint = myVfs.mount('/m'); + const mountPoint = myVfs.mount(); return { myVfs, mountPoint }; } diff --git a/test/parallel/test-vfs-fs-mkdirSync.js b/test/parallel/test-vfs-fs-mkdirSync.js index b532c331d6f914..83942b8856bed8 100644 --- a/test/parallel/test-vfs-fs-mkdirSync.js +++ b/test/parallel/test-vfs-fs-mkdirSync.js @@ -11,7 +11,7 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); -const mountPoint = myVfs.mount('/mkdirSync'); +const mountPoint = myVfs.mount(); // Plain mkdir fs.mkdirSync(path.join(mountPoint, 'src/d1')); diff --git a/test/parallel/test-vfs-fs-mkdtempSync.js b/test/parallel/test-vfs-fs-mkdtempSync.js index b1cf48e5a7b0a0..2fd1e81d709374 100644 --- a/test/parallel/test-vfs-fs-mkdtempSync.js +++ b/test/parallel/test-vfs-fs-mkdtempSync.js @@ -12,7 +12,7 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); -const mountPoint = myVfs.mount('/mkdtempSync'); +const mountPoint = myVfs.mount(); const prefix = path.join(mountPoint, 'src/tmp-'); diff --git a/test/parallel/test-vfs-fs-open-callback.js b/test/parallel/test-vfs-fs-open-callback.js index 75b51a345bb03b..23a4f8ccadb91a 100644 --- a/test/parallel/test-vfs-fs-open-callback.js +++ b/test/parallel/test-vfs-fs-open-callback.js @@ -14,7 +14,7 @@ function mounted() { const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); - const mountPoint = myVfs.mount('/m'); + const mountPoint = myVfs.mount(); return { myVfs, mountPoint }; } diff --git a/test/parallel/test-vfs-fs-openAsBlob.js b/test/parallel/test-vfs-fs-openAsBlob.js index 83029bfc2232ee..663472269156a3 100644 --- a/test/parallel/test-vfs-fs-openAsBlob.js +++ b/test/parallel/test-vfs-fs-openAsBlob.js @@ -12,7 +12,7 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); -const mountPoint = myVfs.mount('/openAsBlob'); +const mountPoint = myVfs.mount(); fs.openAsBlob(path.join(mountPoint, 'src/hello.txt')) .then(async (blob) => { diff --git a/test/parallel/test-vfs-fs-openSync.js b/test/parallel/test-vfs-fs-openSync.js index 9fcfeecedf0a10..a6096f6e9a099d 100644 --- a/test/parallel/test-vfs-fs-openSync.js +++ b/test/parallel/test-vfs-fs-openSync.js @@ -15,7 +15,7 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); -const mountPoint = myVfs.mount('/openSync'); +const mountPoint = myVfs.mount(); // openSync + fstatSync + readSync + closeSync { diff --git a/test/parallel/test-vfs-fs-opendir-callback.js b/test/parallel/test-vfs-fs-opendir-callback.js index 7654a36dbbf925..6b1f6fe5f884c8 100644 --- a/test/parallel/test-vfs-fs-opendir-callback.js +++ b/test/parallel/test-vfs-fs-opendir-callback.js @@ -15,7 +15,7 @@ function mounted() { myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); myVfs.writeFileSync('/src/data.json', '{}'); - const mountPoint = myVfs.mount('/m'); + const mountPoint = myVfs.mount(); return { myVfs, mountPoint }; } diff --git a/test/parallel/test-vfs-fs-opendirSync.js b/test/parallel/test-vfs-fs-opendirSync.js index 6eaf91c97efd5a..260b3114d1045f 100644 --- a/test/parallel/test-vfs-fs-opendirSync.js +++ b/test/parallel/test-vfs-fs-opendirSync.js @@ -13,7 +13,7 @@ const myVfs = vfs.create(); myVfs.mkdirSync('/src/subdir', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); myVfs.writeFileSync('/src/data.json', '{}'); -const mountPoint = myVfs.mount('/opendirSync'); +const mountPoint = myVfs.mount(); const dir = fs.opendirSync(path.join(mountPoint, 'src')); const names = []; diff --git a/test/parallel/test-vfs-fs-promises-buffer-encoding.js b/test/parallel/test-vfs-fs-promises-buffer-encoding.js index c2bcefa188e69e..0c371c9c4587a2 100644 --- a/test/parallel/test-vfs-fs-promises-buffer-encoding.js +++ b/test/parallel/test-vfs-fs-promises-buffer-encoding.js @@ -14,7 +14,7 @@ function mounted() { const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); - const mountPoint = myVfs.mount('/m'); + const mountPoint = myVfs.mount(); return { myVfs, mountPoint }; } diff --git a/test/parallel/test-vfs-fs-promises-stat-no-throw.js b/test/parallel/test-vfs-fs-promises-stat-no-throw.js index 9f343fb5ad9ed7..0b9b28ea10123a 100644 --- a/test/parallel/test-vfs-fs-promises-stat-no-throw.js +++ b/test/parallel/test-vfs-fs-promises-stat-no-throw.js @@ -14,7 +14,7 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); - const mountPoint = myVfs.mount('/stat-no-throw'); + const mountPoint = myVfs.mount(); // Missing file -> undefined const missing = await fsp.stat(path.join(mountPoint, 'src/nope'), diff --git a/test/parallel/test-vfs-fs-promises.js b/test/parallel/test-vfs-fs-promises.js index c24ae480a5dd7e..d1faaf8b7645bc 100644 --- a/test/parallel/test-vfs-fs-promises.js +++ b/test/parallel/test-vfs-fs-promises.js @@ -15,7 +15,7 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); - const mountPoint = myVfs.mount('/promises'); + const mountPoint = myVfs.mount(); const p = (s) => path.join(mountPoint, s); // Path-based reads diff --git a/test/parallel/test-vfs-fs-readFile-callback.js b/test/parallel/test-vfs-fs-readFile-callback.js index 6489d4364dbbc1..653e79e395a277 100644 --- a/test/parallel/test-vfs-fs-readFile-callback.js +++ b/test/parallel/test-vfs-fs-readFile-callback.js @@ -14,7 +14,7 @@ function mounted() { const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); - const mountPoint = myVfs.mount('/m'); + const mountPoint = myVfs.mount(); return { myVfs, mountPoint }; } diff --git a/test/parallel/test-vfs-fs-readFileSync.js b/test/parallel/test-vfs-fs-readFileSync.js index f496b23b87d2a4..297a8855101483 100644 --- a/test/parallel/test-vfs-fs-readFileSync.js +++ b/test/parallel/test-vfs-fs-readFileSync.js @@ -12,7 +12,7 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); -const mountPoint = myVfs.mount('/readFileSync'); +const mountPoint = myVfs.mount(); // Default (buffer) result { @@ -52,7 +52,7 @@ myVfs.unmount(); const realVfs = vfs.create(new vfs.RealFSProvider(root), { emitExperimentalWarning: false }); - const realMountPoint = realVfs.mount('/real'); + const realMountPoint = realVfs.mount(); try { fs.writeFileSync(path.join(root, 'a.txt'), 'still readable'); const fd = fs.openSync(path.join(realMountPoint, 'a.txt'), 'r'); diff --git a/test/parallel/test-vfs-fs-readdirSync.js b/test/parallel/test-vfs-fs-readdirSync.js index 1fe629f5f442b0..ee1a1a258b3cfc 100644 --- a/test/parallel/test-vfs-fs-readdirSync.js +++ b/test/parallel/test-vfs-fs-readdirSync.js @@ -14,7 +14,7 @@ const myVfs = vfs.create(); myVfs.mkdirSync('/src/subdir', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); myVfs.writeFileSync('/src/data.json', '{}'); -const mountPoint = myVfs.mount('/readdirSync'); +const mountPoint = myVfs.mount(); // Default (utf8 string array) { diff --git a/test/parallel/test-vfs-fs-realpathSync.js b/test/parallel/test-vfs-fs-realpathSync.js index 53d46cba3a002f..2d87b1e87d3b49 100644 --- a/test/parallel/test-vfs-fs-realpathSync.js +++ b/test/parallel/test-vfs-fs-realpathSync.js @@ -12,7 +12,7 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); -const mountPoint = myVfs.mount('/realpathSync'); +const mountPoint = myVfs.mount(); const p = path.join(mountPoint, 'src/hello.txt'); diff --git a/test/parallel/test-vfs-fs-rename-callback.js b/test/parallel/test-vfs-fs-rename-callback.js index 84e2d1d8b0e31d..dd426c181e4aa5 100644 --- a/test/parallel/test-vfs-fs-rename-callback.js +++ b/test/parallel/test-vfs-fs-rename-callback.js @@ -13,7 +13,7 @@ function mounted() { const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); - const mountPoint = myVfs.mount('/m'); + const mountPoint = myVfs.mount(); return { myVfs, mountPoint }; } diff --git a/test/parallel/test-vfs-fs-renameSync.js b/test/parallel/test-vfs-fs-renameSync.js index c15885159d402b..1f1cbf6b394d5c 100644 --- a/test/parallel/test-vfs-fs-renameSync.js +++ b/test/parallel/test-vfs-fs-renameSync.js @@ -12,7 +12,7 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); -const mountPoint = myVfs.mount('/renameSync'); +const mountPoint = myVfs.mount(); fs.renameSync( path.join(mountPoint, 'src/hello.txt'), diff --git a/test/parallel/test-vfs-fs-rmSync.js b/test/parallel/test-vfs-fs-rmSync.js index b239738df884a2..4f3af281d61a77 100644 --- a/test/parallel/test-vfs-fs-rmSync.js +++ b/test/parallel/test-vfs-fs-rmSync.js @@ -14,7 +14,7 @@ myVfs.mkdirSync('/src/subdir', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); myVfs.writeFileSync('/src/subdir/inside.txt', 'inside'); myVfs.mkdirSync('/empty'); -const mountPoint = myVfs.mount('/rmSync'); +const mountPoint = myVfs.mount(); // rmdirSync on an empty directory fs.rmdirSync(path.join(mountPoint, 'empty')); diff --git a/test/parallel/test-vfs-fs-stat-callback.js b/test/parallel/test-vfs-fs-stat-callback.js index de0860593ddd5b..bd3494b73feccb 100644 --- a/test/parallel/test-vfs-fs-stat-callback.js +++ b/test/parallel/test-vfs-fs-stat-callback.js @@ -13,7 +13,7 @@ function mounted() { const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); - const mountPoint = myVfs.mount('/m'); + const mountPoint = myVfs.mount(); return { myVfs, mountPoint }; } diff --git a/test/parallel/test-vfs-fs-statSync.js b/test/parallel/test-vfs-fs-statSync.js index 46791319b6ce69..5a48c4e426fa5a 100644 --- a/test/parallel/test-vfs-fs-statSync.js +++ b/test/parallel/test-vfs-fs-statSync.js @@ -13,7 +13,7 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); -const mountPoint = myVfs.mount('/statSync'); +const mountPoint = myVfs.mount(); // statSync on a regular file { diff --git a/test/parallel/test-vfs-fs-symlink-callback.js b/test/parallel/test-vfs-fs-symlink-callback.js index e5af84e8d41b47..e35eeb38b980d9 100644 --- a/test/parallel/test-vfs-fs-symlink-callback.js +++ b/test/parallel/test-vfs-fs-symlink-callback.js @@ -12,7 +12,7 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); -const mountPoint = myVfs.mount('/symlink-cb'); +const mountPoint = myVfs.mount(); fs.symlink('hello.txt', path.join(mountPoint, 'src/lnk.txt'), common.mustSucceed(() => { diff --git a/test/parallel/test-vfs-fs-symlinkSync.js b/test/parallel/test-vfs-fs-symlinkSync.js index 5787a9908f6f57..5a39cc281e0598 100644 --- a/test/parallel/test-vfs-fs-symlinkSync.js +++ b/test/parallel/test-vfs-fs-symlinkSync.js @@ -13,7 +13,7 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); -const mountPoint = myVfs.mount('/symlinkSync'); +const mountPoint = myVfs.mount(); fs.symlinkSync('hello.txt', path.join(mountPoint, 'src/link.txt')); assert.strictEqual( diff --git a/test/parallel/test-vfs-fs-truncate-callback.js b/test/parallel/test-vfs-fs-truncate-callback.js index 3d218594dc9934..1399bf6ed149a0 100644 --- a/test/parallel/test-vfs-fs-truncate-callback.js +++ b/test/parallel/test-vfs-fs-truncate-callback.js @@ -12,7 +12,7 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); -const mountPoint = myVfs.mount('/truncate-cb'); +const mountPoint = myVfs.mount(); fs.truncate(path.join(mountPoint, 'src/hello.txt'), 5, common.mustSucceed(() => { diff --git a/test/parallel/test-vfs-fs-truncateSync.js b/test/parallel/test-vfs-fs-truncateSync.js index 3ece38fe305f53..d4ed16f8fb87dc 100644 --- a/test/parallel/test-vfs-fs-truncateSync.js +++ b/test/parallel/test-vfs-fs-truncateSync.js @@ -12,7 +12,7 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); -const mountPoint = myVfs.mount('/truncateSync'); +const mountPoint = myVfs.mount(); fs.truncateSync(path.join(mountPoint, 'src/hello.txt'), 5); assert.strictEqual( diff --git a/test/parallel/test-vfs-fs-watch-dispatch.js b/test/parallel/test-vfs-fs-watch-dispatch.js index 818e5a28a47bf1..e9952919193bce 100644 --- a/test/parallel/test-vfs-fs-watch-dispatch.js +++ b/test/parallel/test-vfs-fs-watch-dispatch.js @@ -13,7 +13,7 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello'); -const mountPoint = myVfs.mount('/watch-dispatch'); +const mountPoint = myVfs.mount(); const watcher = fs.watch(path.join(mountPoint, 'src/hello.txt')); assert.ok(watcher); diff --git a/test/parallel/test-vfs-fs-writeFile-callback.js b/test/parallel/test-vfs-fs-writeFile-callback.js index c3af45120403fb..ce22b6b8811ac3 100644 --- a/test/parallel/test-vfs-fs-writeFile-callback.js +++ b/test/parallel/test-vfs-fs-writeFile-callback.js @@ -12,7 +12,7 @@ const vfs = require('node:vfs'); function mounted() { const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); - const mountPoint = myVfs.mount('/m'); + const mountPoint = myVfs.mount(); return { myVfs, mountPoint }; } diff --git a/test/parallel/test-vfs-fs-writeFileSync.js b/test/parallel/test-vfs-fs-writeFileSync.js index fed015b6c94ee2..7bde5f6c7b85b7 100644 --- a/test/parallel/test-vfs-fs-writeFileSync.js +++ b/test/parallel/test-vfs-fs-writeFileSync.js @@ -11,7 +11,7 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); -const mountPoint = myVfs.mount('/writeFileSync'); +const mountPoint = myVfs.mount(); const target = path.join(mountPoint, 'src/new.txt'); @@ -62,7 +62,7 @@ myVfs.unmount(); const realVfs = vfs.create( new vfs.RealFSProvider(root), { emitExperimentalWarning: false }); - const realMountPoint = realVfs.mount('/real-writeFileSync'); + const realMountPoint = realVfs.mount(); try { const mountedFile = path.join(realMountPoint, 'a.txt'); fs.writeFileSync(path.join(root, 'a.txt'), 'old'); diff --git a/test/parallel/test-vfs-import.mjs b/test/parallel/test-vfs-import.mjs index 4455d4ad7fd371..a3eea83f5a147b 100644 --- a/test/parallel/test-vfs-import.mjs +++ b/test/parallel/test-vfs-import.mjs @@ -14,7 +14,7 @@ const vfsImport = (path) => pathToFileURL(path).href; { const myVfs = vfs.create(); myVfs.writeFileSync('/hello.mjs', 'export const message = "hello from vfs";'); - const mountPoint = myVfs.mount('/esm-named'); + const mountPoint = myVfs.mount(); const { message } = await import(vfsImport(`${mountPoint}/hello.mjs`)); assert.strictEqual(message, 'hello from vfs'); @@ -26,7 +26,7 @@ const vfsImport = (path) => pathToFileURL(path).href; { const myVfs = vfs.create(); myVfs.writeFileSync('/default.mjs', 'export default { name: "test", value: 42 };'); - const mountPoint = myVfs.mount('/esm-default'); + const mountPoint = myVfs.mount(); const mod = await import(vfsImport(`${mountPoint}/default.mjs`)); assert.strictEqual(mod.default.name, 'test'); @@ -39,7 +39,7 @@ const vfsImport = (path) => pathToFileURL(path).href; // Mount first so the embedded absolute specifier can use the mount point. { const myVfs = vfs.create(); - const mountPoint = myVfs.mount('/esm-chain'); + const mountPoint = myVfs.mount(); myVfs.writeFileSync(`${mountPoint}/utils.mjs`, 'export function add(a, b) { return a + b; }'); myVfs.writeFileSync(`${mountPoint}/main.mjs`, ` @@ -62,7 +62,7 @@ const vfsImport = (path) => pathToFileURL(path).href; import { helper } from './helper.mjs'; export const output = helper(); `); - const mountPoint = myVfs.mount('/esm-relative'); + const mountPoint = myVfs.mount(); const { output } = await import(vfsImport(`${mountPoint}/lib/index.mjs`)); assert.strictEqual(output, 'helped'); @@ -74,7 +74,7 @@ const vfsImport = (path) => pathToFileURL(path).href; { const myVfs = vfs.create(); myVfs.writeFileSync('/data.json', JSON.stringify({ items: [1, 2, 3], enabled: true })); - const mountPoint = myVfs.mount('/esm-json'); + const mountPoint = myVfs.mount(); const data = await import(vfsImport(`${mountPoint}/data.json`), { with: { type: 'json' } }); assert.deepStrictEqual(data.default.items, [1, 2, 3]); @@ -87,7 +87,7 @@ const vfsImport = (path) => pathToFileURL(path).href; { const myVfs = vfs.create(); myVfs.writeFileSync('/test.mjs', 'export const x = 1;'); - myVfs.mount('/esm-builtin'); + myVfs.mount(); // Import from node: should still work const assertMod = await import('node:assert'); @@ -101,7 +101,7 @@ const vfsImport = (path) => pathToFileURL(path).href; const myVfs = vfs.create(); myVfs.writeFileSync('/esm-module.mjs', 'export const esmValue = "esm";'); myVfs.writeFileSync('/cjs-module.js', 'module.exports = { cjsValue: "cjs" };'); - const mountPoint = myVfs.mount('/esm-mixed'); + const mountPoint = myVfs.mount(); const { esmValue } = await import(vfsImport(`${mountPoint}/esm-module.mjs`)); assert.strictEqual(esmValue, 'esm'); @@ -138,7 +138,7 @@ const vfsImport = (path) => pathToFileURL(path).href; '/app/entry.mjs', "export { fromVfs } from 'my-vfs-pkg';", ); - const mountPoint = myVfs.mount('/esm-bare'); + const mountPoint = myVfs.mount(); const { fromVfs } = await import(vfsImport(`${mountPoint}/app/entry.mjs`)); assert.strictEqual(fromVfs, true); diff --git a/test/parallel/test-vfs-invalid-package-json.js b/test/parallel/test-vfs-invalid-package-json.js index 17d0e88fafa4ad..2c87eb5d5e4cd7 100644 --- a/test/parallel/test-vfs-invalid-package-json.js +++ b/test/parallel/test-vfs-invalid-package-json.js @@ -12,7 +12,7 @@ const vfs = require('node:vfs'); myVfs.mkdirSync('/pkg', { recursive: true }); myVfs.writeFileSync('/pkg/package.json', '{ invalid json'); myVfs.writeFileSync('/pkg/index.js', 'module.exports = 42;'); - const mountPoint = myVfs.mount('/mnt'); + const mountPoint = myVfs.mount(); assert.strictEqual(require(`${mountPoint}/pkg`), 42); @@ -25,7 +25,7 @@ const vfs = require('node:vfs'); myVfs.mkdirSync('/pkg2', { recursive: true }); myVfs.writeFileSync('/pkg2/package.json', '{"main": "lib.js"}'); myVfs.writeFileSync('/pkg2/lib.js', 'module.exports = 99;'); - const mountPoint = myVfs.mount('/mnt2'); + const mountPoint = myVfs.mount(); assert.strictEqual(require(`${mountPoint}/pkg2`), 99); @@ -37,7 +37,7 @@ const vfs = require('node:vfs'); const myVfs = vfs.create(); myVfs.mkdirSync('/pkg3', { recursive: true }); myVfs.writeFileSync('/pkg3/index.js', 'module.exports = 77;'); - const mountPoint = myVfs.mount('/mnt3'); + const mountPoint = myVfs.mount(); assert.strictEqual(require(`${mountPoint}/pkg3`), 77); diff --git a/test/parallel/test-vfs-layer-id.js b/test/parallel/test-vfs-layer-id.js index a52211425396cd..548d2e52bc5256 100644 --- a/test/parallel/test-vfs-layer-id.js +++ b/test/parallel/test-vfs-layer-id.js @@ -20,7 +20,7 @@ const vfs = require('node:vfs'); assert.ok(c.layerId > b.layerId); // Stable across mount/unmount. const idBefore = a.layerId; - a.mount('/mnt-layer-stable'); + a.mount(); assert.strictEqual(a.layerId, idBefore); a.unmount(); assert.strictEqual(a.layerId, idBefore); @@ -30,9 +30,9 @@ const vfs = require('node:vfs'); // of any VFS path is visible in the path itself. { const v = vfs.create(); - const mountPoint = v.mount('/data'); + const mountPoint = v.mount(); assert.strictEqual( mountPoint, - path.join(os.devNull, 'vfs', `layer-${v.layerId}`, 'data')); + path.join(os.devNull, 'vfs', String(v.layerId))); v.unmount(); } diff --git a/test/parallel/test-vfs-layer-tag-prefix.mjs b/test/parallel/test-vfs-layer-tag-prefix.mjs index 8f21c82149f7d0..d1356e023801c0 100644 --- a/test/parallel/test-vfs-layer-tag-prefix.mjs +++ b/test/parallel/test-vfs-layer-tag-prefix.mjs @@ -1,7 +1,7 @@ // Flags: --experimental-vfs // Layer identity is carried by the mount-point path itself -// (`${os.devNull}/vfs/layer-/...`), so unmounting one layer must +// (`${os.devNull}/vfs/`), so unmounting one layer must // purge exactly that layer's ESM cache entries - layers whose ids // share a decimal prefix (1 vs 10) must be unaffected. @@ -31,21 +31,21 @@ assert.strictEqual(layerTen.layerId, 10); assert.ok(String(layerTen.layerId).startsWith(String(layerOne.layerId)), 'test scaffolding: 10 must have 1 as a string prefix'); -layerOne.writeFileSync('/m.mjs', 'export const tag = "layer-one";'); -const mountOne = layerOne.mount('/mnt-tag'); +layerOne.writeFileSync('/m.mjs', 'export const tag = "one";'); +const mountOne = layerOne.mount(); -layerTen.writeFileSync('/m.mjs', 'export const tag = "layer-ten";'); -const mountTen = layerTen.mount('/mnt-tag'); +layerTen.writeFileSync('/m.mjs', 'export const tag = "ten";'); +const mountTen = layerTen.mount(); // Warm both ESM cache entries. const oneA = await import(vfsImport(`${mountOne}/m.mjs`)); const tenA = await import(vfsImport(`${mountTen}/m.mjs`)); -assert.strictEqual(oneA.tag, 'layer-one'); -assert.strictEqual(tenA.tag, 'layer-ten'); +assert.strictEqual(oneA.tag, 'one'); +assert.strictEqual(tenA.tag, 'ten'); // Unmount layer 1. Layer 10's cache entry must survive: its mount -// point is `.../layer-10/mnt-tag`, which is not under -// `.../layer-1/mnt-tag` even though "1" is a decimal prefix of "10". +// point is `.../10`, which is not under `.../1` even though "1" is a +// decimal prefix of "10". layerOne.unmount(); // Layer 10 is still mounted; importing again must resolve to the same diff --git a/test/parallel/test-vfs-module-hooks-cleanup.js b/test/parallel/test-vfs-module-hooks-cleanup.js index 7b784d32ec0c00..cb14a4859fff71 100644 --- a/test/parallel/test-vfs-module-hooks-cleanup.js +++ b/test/parallel/test-vfs-module-hooks-cleanup.js @@ -17,13 +17,13 @@ const vfsImport = (path) => pathToFileURL(path).href; { const a = vfs.create(); a.writeFileSync('/m1.js', 'module.exports = "first"'); - const mountA = a.mount('/mnt-cycle-1'); + const mountA = a.mount(); assert.strictEqual(require(`${mountA}/m1.js`), 'first'); a.unmount(); const b = vfs.create(); b.writeFileSync('/m2.js', 'module.exports = "second"'); - const mountB = b.mount('/mnt-cycle-2'); + const mountB = b.mount(); assert.strictEqual(require(`${mountB}/m2.js`), 'second'); b.unmount(); } @@ -33,7 +33,7 @@ const vfsImport = (path) => pathToFileURL(path).href; { const v = vfs.create(); v.writeFileSync('/x.js', 'module.exports = 1'); - const mountPoint = v.mount('/mnt-cleanup'); + const mountPoint = v.mount(); require(`${mountPoint}/x.js`); v.unmount(); const fs = require('fs'); @@ -48,7 +48,7 @@ const vfsImport = (path) => pathToFileURL(path).href; v.mkdirSync('/pkg'); v.writeFileSync('/pkg/package.json', 'null'); v.writeFileSync('/pkg/index.js', 'module.exports = 1'); - const mountPoint = v.mount('/mnt-null-pjson'); + const mountPoint = v.mount(); assert.throws( () => require(`${mountPoint}/pkg`), { code: 'ERR_INVALID_PACKAGE_CONFIG' }, @@ -64,7 +64,7 @@ const vfsImport = (path) => pathToFileURL(path).href; v.mkdirSync('/pkg'); v.writeFileSync('/pkg/package.json', '{"main": 42}'); v.writeFileSync('/pkg/index.js', 'module.exports = "via-index"'); - const mountPoint = v.mount('/mnt-lax-main'); + const mountPoint = v.mount(); assert.strictEqual(require(`${mountPoint}/pkg`), 'via-index'); v.unmount(); } @@ -75,10 +75,10 @@ const vfsImport = (path) => pathToFileURL(path).href; { const a = vfs.create(); a.writeFileSync('/a.js', 'module.exports = "a"'); - const mountA = a.mount('/mnt-multi-a'); + const mountA = a.mount(); const b = vfs.create(); b.writeFileSync('/b.js', 'module.exports = "b"'); - const mountB = b.mount('/mnt-multi-b'); + const mountB = b.mount(); assert.strictEqual(require(`${mountA}/a.js`), 'a'); assert.strictEqual(require(`${mountB}/b.js`), 'b'); @@ -103,7 +103,7 @@ const vfsImport = (path) => pathToFileURL(path).href; v.writeFileSync( '/app/node_modules/badpkg/package.json', '{"main": "./nope.js"}'); v.writeFileSync('/app/entry.mjs', "import 'badpkg';"); - const mountPoint = v.mount('/mnt-legacy-err'); + const mountPoint = v.mount(); await assert.rejects( () => import(vfsImport(`${mountPoint}/app/entry.mjs`)), (err) => { @@ -127,12 +127,12 @@ const vfsImport = (path) => pathToFileURL(path).href; v.mkdirSync('/real', { recursive: true }); v.writeFileSync('/real/mod.js', 'module.exports = "one"'); v.symlinkSync('/real/mod.js', '/link.js'); - const mountPoint = v.mount('/mnt-symlink'); + const mountPoint = v.mount(); assert.strictEqual(require(`${mountPoint}/link.js`), 'one'); v.unmount(); v.writeFileSync('/real/mod.js', 'module.exports = "two"'); - const mountPoint2 = v.mount('/mnt-symlink'); + const mountPoint2 = v.mount(); assert.strictEqual(mountPoint2, mountPoint); assert.strictEqual(require(`${mountPoint2}/link.js`), 'two'); v.unmount(); diff --git a/test/parallel/test-vfs-module-hooks.mjs b/test/parallel/test-vfs-module-hooks.mjs index e215c2c48f176e..833333d144941e 100644 --- a/test/parallel/test-vfs-module-hooks.mjs +++ b/test/parallel/test-vfs-module-hooks.mjs @@ -31,7 +31,7 @@ const vfsImport = (path) => pathToFileURL(path).href; '/app/entry.js', "module.exports = require('str-pkg');", ); - const mountPoint = myVfs.mount('/mh1'); + const mountPoint = myVfs.mount(); const result = require(`${mountPoint}/app/entry.js`); assert.strictEqual(result.strExport, true); @@ -75,7 +75,7 @@ const vfsImport = (path) => pathToFileURL(path).href; '/app/cjs-entry.js', "module.exports = require('cond-pkg');", ); - const mountPoint = myVfs.mount('/mh2'); + const mountPoint = myVfs.mount(); // ESM import should get the 'import' condition const esmResult = await import(vfsImport(`${mountPoint}/app/esm-entry.mjs`)); @@ -117,7 +117,7 @@ const vfsImport = (path) => pathToFileURL(path).href; export { main, feature }; `, ); - const mountPoint = myVfs.mount('/mh3'); + const mountPoint = myVfs.mount(); const result = await import(vfsImport(`${mountPoint}/app/entry.mjs`)); assert.strictEqual(result.main, true); @@ -153,7 +153,7 @@ const vfsImport = (path) => pathToFileURL(path).href; '/app/entry2.mjs', "export { fromSubCond } from 'sub-cond-pkg';", ); - const mountPoint = myVfs.mount('/mh4'); + const mountPoint = myVfs.mount(); const result = await import(vfsImport(`${mountPoint}/app/entry2.mjs`)); assert.strictEqual(result.fromSubCond, 'esm'); @@ -193,7 +193,7 @@ const vfsImport = (path) => pathToFileURL(path).href; '/app/entry3.mjs', "export { nested } from 'nested-cond-pkg';", ); - const mountPoint = myVfs.mount('/mh5'); + const mountPoint = myVfs.mount(); const result = await import(vfsImport(`${mountPoint}/app/entry3.mjs`)); assert.strictEqual(result.nested, 'node-esm'); @@ -220,7 +220,7 @@ const vfsImport = (path) => pathToFileURL(path).href; '/app/entry4.js', "module.exports = require('main-pkg');", ); - const mountPoint = myVfs.mount('/mh6'); + const mountPoint = myVfs.mount(); const result = require(`${mountPoint}/app/entry4.js`); assert.strictEqual(result.fromMain, true); @@ -245,7 +245,7 @@ const vfsImport = (path) => pathToFileURL(path).href; '/app/entry5.js', "module.exports = require('index-pkg');", ); - const mountPoint = myVfs.mount('/mh7'); + const mountPoint = myVfs.mount(); const result = require(`${mountPoint}/app/entry5.js`); assert.strictEqual(result.fromIndex, true); @@ -260,7 +260,7 @@ const vfsImport = (path) => pathToFileURL(path).href; const myVfs = vfs.create(); // The embedded specifier must be an absolute mounted path, so mount // first and write the files through the mounted paths. - const mountPoint = myVfs.mount('/mh8'); + const mountPoint = myVfs.mount(); myVfs.mkdirSync(`${mountPoint}/lib`, { recursive: true }); myVfs.writeFileSync(`${mountPoint}/lib/utils.js`, 'module.exports = { ext: "js" };'); // File without .js extension in specifier @@ -282,7 +282,7 @@ const vfsImport = (path) => pathToFileURL(path).href; const myVfs = vfs.create(); // The embedded specifier must be an absolute mounted path, so mount // first and write the files through the mounted paths. - const mountPoint = myVfs.mount('/mh9'); + const mountPoint = myVfs.mount(); myVfs.writeFileSync(`${mountPoint}/data.json`, JSON.stringify({ ext: 'json' })); myVfs.writeFileSync( `${mountPoint}/reader.js`, @@ -314,7 +314,7 @@ const vfsImport = (path) => pathToFileURL(path).href; '/app/scoped-entry.mjs', "export { scoped } from '@myorg/mylib';", ); - const mountPoint = myVfs.mount('/mh11'); + const mountPoint = myVfs.mount(); const result = await import(vfsImport(`${mountPoint}/app/scoped-entry.mjs`)); assert.strictEqual(result.scoped, true); @@ -350,7 +350,7 @@ const vfsImport = (path) => pathToFileURL(path).href; export { helpers }; `, ); - const mountPoint = myVfs.mount('/mh12'); + const mountPoint = myVfs.mount(); const result = await import(vfsImport(`${mountPoint}/app/scoped-sub-entry.mjs`)); assert.strictEqual(result.helpers, true); @@ -365,7 +365,7 @@ const vfsImport = (path) => pathToFileURL(path).href; const myVfs = vfs.create(); myVfs.writeFileSync('/package.json', JSON.stringify({ type: 'module' })); myVfs.writeFileSync('/mod.js', 'export const fromModule = true;'); - const mountPoint = myVfs.mount('/mh13'); + const mountPoint = myVfs.mount(); const result = await import(vfsImport(`${mountPoint}/mod.js`)); assert.strictEqual(result.fromModule, true); @@ -380,7 +380,7 @@ const vfsImport = (path) => pathToFileURL(path).href; const myVfs = vfs.create(); // The embedded specifier must be an absolute mounted path, so mount // first and write the files through the mounted paths. - const mountPoint = myVfs.mount('/mh14'); + const mountPoint = myVfs.mount(); myVfs.writeFileSync(`${mountPoint}/package.json`, JSON.stringify({ type: 'module' })); myVfs.writeFileSync(`${mountPoint}/helper.cjs`, 'module.exports = { cjsAlways: true };'); myVfs.writeFileSync( @@ -405,7 +405,7 @@ const vfsImport = (path) => pathToFileURL(path).href; { const myVfs = vfs.create(); myVfs.writeFileSync('/fileurl.mjs', 'export const fromFileUrl = true;'); - const mountPoint = myVfs.mount('/mh15'); + const mountPoint = myVfs.mount(); const result = await import(vfsImport(`${mountPoint}/fileurl.mjs`)); assert.strictEqual(result.fromFileUrl, true); @@ -427,7 +427,7 @@ const vfsImport = (path) => pathToFileURL(path).href; '/dir-pkg/entry.js', 'module.exports = { dirPkg: true };', ); - const mountPoint = myVfs.mount('/mh16'); + const mountPoint = myVfs.mount(); // Main field has no extension - tryExtensions should resolve entry → entry.js const result = require(`${mountPoint}/dir-pkg`); @@ -453,7 +453,7 @@ const vfsImport = (path) => pathToFileURL(path).href; '/app/entry-sub.js', "module.exports = require('direct-pkg/lib/util.js');", ); - const mountPoint = myVfs.mount('/mh17'); + const mountPoint = myVfs.mount(); const result = require(`${mountPoint}/app/entry-sub.js`); assert.strictEqual(result.directSub, true); @@ -479,7 +479,7 @@ const vfsImport = (path) => pathToFileURL(path).href; // No .js extension - should be resolved by tryExtensions "module.exports = require('ext-pkg/lib/util');", ); - const mountPoint = myVfs.mount('/mh18'); + const mountPoint = myVfs.mount(); const result = require(`${mountPoint}/app/entry-ext.js`); assert.strictEqual(result.extSub, true); @@ -505,7 +505,7 @@ const vfsImport = (path) => pathToFileURL(path).href; '/app/entry-main-ext.js', "module.exports = require('main-ext-pkg');", ); - const mountPoint = myVfs.mount('/mh19'); + const mountPoint = myVfs.mount(); const result = require(`${mountPoint}/app/entry-main-ext.js`); assert.strictEqual(result.mainExt, true); @@ -534,7 +534,7 @@ const vfsImport = (path) => pathToFileURL(path).href; '/app/entry-arr.js', "module.exports = require('arr-exp-pkg');", ); - const mountPoint = myVfs.mount('/mh22'); + const mountPoint = myVfs.mount(); // Array target in exports: canonical resolver tries each entry in order const result = require(`${mountPoint}/app/entry-arr.js`); @@ -566,7 +566,7 @@ const vfsImport = (path) => pathToFileURL(path).href; '/app/entry-default.mjs', "export { fromDefault } from 'default-pkg';", ); - const mountPoint = myVfs.mount('/mh23'); + const mountPoint = myVfs.mount(); // 'browser' condition not active in Node, 'default' should match const result = await import(vfsImport(`${mountPoint}/app/entry-default.mjs`)); @@ -582,7 +582,7 @@ const vfsImport = (path) => pathToFileURL(path).href; const myVfs = vfs.create(); myVfs.writeFileSync('/package.json', JSON.stringify({ type: 'commonjs' })); myVfs.writeFileSync('/explicit-cjs.js', 'module.exports = { explicitCjs: true };'); - const mountPoint = myVfs.mount('/mh24'); + const mountPoint = myVfs.mount(); const result = require(`${mountPoint}/explicit-cjs.js`); assert.strictEqual(result.explicitCjs, true); @@ -596,7 +596,7 @@ const vfsImport = (path) => pathToFileURL(path).href; { const myVfs = vfs.create(); myVfs.writeFileSync('/no-pkg.js', 'module.exports = { noPkg: true };'); - const mountPoint = myVfs.mount('/mh25'); + const mountPoint = myVfs.mount(); const result = require(`${mountPoint}/no-pkg.js`); assert.strictEqual(result.noPkg, true); @@ -617,7 +617,7 @@ const vfsImport = (path) => pathToFileURL(path).href; '/node_modules/inner/index.js', 'module.exports = { inner: true };', ); - const mountPoint = myVfs.mount('/mh26'); + const mountPoint = myVfs.mount(); // The walk should stop at node_modules, not inherit type:module from root const result = require(`${mountPoint}/node_modules/inner/index.js`); @@ -637,7 +637,7 @@ const vfsImport = (path) => pathToFileURL(path).href; '/bad-json-dir/index.js', 'module.exports = { fallbackIndex: true };', ); - const mountPoint = myVfs.mount('/mh28'); + const mountPoint = myVfs.mount(); // Should fall through to index.js after failing to parse package.json const result = require(`${mountPoint}/bad-json-dir`); @@ -653,7 +653,7 @@ const vfsImport = (path) => pathToFileURL(path).href; const myVfs = vfs.create(); myVfs.writeFileSync('/package.json', '{ broken json }'); myVfs.writeFileSync('/no-type.js', 'module.exports = { noType: true };'); - const mountPoint = myVfs.mount('/mh29'); + const mountPoint = myVfs.mount(); // Should treat as 'none' (commonjs) since package.json is invalid const result = require(`${mountPoint}/no-type.js`); @@ -680,7 +680,7 @@ const vfsImport = (path) => pathToFileURL(path).href; '/app/entry-solo.js', "module.exports = require('@solo/pkg');", ); - const mountPoint = myVfs.mount('/mh30'); + const mountPoint = myVfs.mount(); const result = require(`${mountPoint}/app/entry-solo.js`); assert.strictEqual(result.solo, true); @@ -697,7 +697,7 @@ const vfsImport = (path) => pathToFileURL(path).href; import path from 'node:path'; export const sep = path.sep; `); - const mountPoint = myVfs.mount('/mh31'); + const mountPoint = myVfs.mount(); const result = await import(vfsImport(`${mountPoint}/use-builtin.mjs`)); assert.strictEqual(typeof result.sep, 'string'); @@ -711,7 +711,7 @@ const vfsImport = (path) => pathToFileURL(path).href; { const myVfs = vfs.create(); myVfs.writeFileSync('/data.json', JSON.stringify({ preformat: true })); - const mountPoint = myVfs.mount('/mh32'); + const mountPoint = myVfs.mount(); const result = await import(vfsImport(`${mountPoint}/data.json`), { with: { type: 'json' } }); assert.strictEqual(result.default.preformat, true); @@ -725,7 +725,7 @@ const vfsImport = (path) => pathToFileURL(path).href; { const myVfs = vfs.create(); myVfs.writeFileSync('/data.txt', 'module.exports = { txt: true };'); - const mountPoint = myVfs.mount('/mh33'); + const mountPoint = myVfs.mount(); // .txt extension → falls back to 'commonjs' via VFS_FORMAT_MAP default const result = require(`${mountPoint}/data.txt`); diff --git a/test/parallel/test-vfs-mount-errors.js b/test/parallel/test-vfs-mount-errors.js index cfcb4a189a4ff6..dd0fef6064217c 100644 --- a/test/parallel/test-vfs-mount-errors.js +++ b/test/parallel/test-vfs-mount-errors.js @@ -20,8 +20,8 @@ const { vfsState } = require('internal/fs/utils'); const b = vfs.create(); a.writeFileSync('/file.txt', 'a'); b.mkdirSync('/x', { recursive: true }); - const mountA = a.mount('/a'); - const mountB = b.mount('/b'); + const mountA = a.mount(); + const mountB = b.mount(); assert.throws( () => fs.renameSync(path.join(mountA, 'file.txt'), @@ -38,8 +38,8 @@ const { vfsState } = require('internal/fs/utils'); const b = vfs.create(); a.writeFileSync('/file.txt', 'a'); b.mkdirSync('/x', { recursive: true }); - const mountA = a.mount('/a'); - const mountB = b.mount('/b'); + const mountA = a.mount(); + const mountB = b.mount(); assert.throws( () => fs.copyFileSync(path.join(mountA, 'file.txt'), @@ -56,8 +56,8 @@ const { vfsState } = require('internal/fs/utils'); const b = vfs.create(); a.writeFileSync('/file.txt', 'a'); b.mkdirSync('/x', { recursive: true }); - const mountA = a.mount('/a'); - const mountB = b.mount('/b'); + const mountA = a.mount(); + const mountB = b.mount(); assert.throws( () => fs.linkSync(path.join(mountA, 'file.txt'), @@ -72,7 +72,7 @@ const { vfsState } = require('internal/fs/utils'); { const a = vfs.create(); a.writeFileSync('/file.txt', 'a'); - const mountA = a.mount('/a'); + const mountA = a.mount(); const tmpReal = '/tmp/vfs-mount-real-' + process.pid + '.txt'; assert.throws( @@ -86,14 +86,14 @@ const { vfsState } = require('internal/fs/utils'); { assert.strictEqual(vfsState.handlers, null); const x = vfs.create(); - x.mount('/x'); + x.mount(); assert.notStrictEqual(vfsState.handlers, null); x.unmount(); assert.strictEqual(vfsState.handlers, null); // And it re-installs on a subsequent mount const y = vfs.create(); - y.mount('/y'); + y.mount(); assert.notStrictEqual(vfsState.handlers, null); y.unmount(); assert.strictEqual(vfsState.handlers, null); @@ -103,8 +103,8 @@ const { vfsState } = require('internal/fs/utils'); { const a = vfs.create(); const b = vfs.create(); - a.mount('/a'); - b.mount('/b'); + a.mount(); + b.mount(); assert.notStrictEqual(vfsState.handlers, null); a.unmount(); assert.notStrictEqual(vfsState.handlers, null); @@ -119,14 +119,15 @@ const { vfsState } = require('internal/fs/utils'); const b = vfs.create(); a.writeFileSync('/f.txt', 'a'); b.writeFileSync('/f.txt', 'b'); - const mountA = a.mount('/same'); - const mountB = b.mount('/same'); + const mountA = a.mount(); + const mountB = b.mount(); + assert.notStrictEqual(mountA, mountB); b.unmount(); // B's namespace is dead even though A is still mounted. assert.strictEqual(fs.existsSync(path.join(mountB, 'f.txt')), false); assert.strictEqual(fs.readFileSync(path.join(mountA, 'f.txt'), 'utf8'), 'a'); - // A malformed layer path under the VFS root is not served either. - const bogus = path.join(os.devNull, 'vfs', 'not-a-layer', 'f.txt'); + // A malformed layer segment under the VFS root is not served either. + const bogus = path.join(os.devNull, 'vfs', 'not-a-number', 'f.txt'); assert.strictEqual(fs.existsSync(bogus), false); a.unmount(); } @@ -134,7 +135,7 @@ const { vfsState } = require('internal/fs/utils'); // Double-mount of same instance rejected { const a = vfs.create(); - a.mount('/first'); - assert.throws(() => a.mount('/second'), { code: 'ERR_INVALID_STATE' }); + a.mount(); + assert.throws(() => a.mount(), { code: 'ERR_INVALID_STATE' }); a.unmount(); } diff --git a/test/parallel/test-vfs-mount.js b/test/parallel/test-vfs-mount.js index d78e312930b298..6e0c2555a7b9c0 100644 --- a/test/parallel/test-vfs-mount.js +++ b/test/parallel/test-vfs-mount.js @@ -14,7 +14,7 @@ function createMountedVfs() { const myVfs = vfs.create(); myVfs.mkdirSync('/src', { recursive: true }); myVfs.writeFileSync('/src/hello.txt', 'hello world'); - const mountPoint = myVfs.mount('/data'); + const mountPoint = myVfs.mount(); return { myVfs, mountPoint }; } @@ -25,63 +25,42 @@ function createMountedVfs() { assert.strictEqual(myVfs.mounted, false); assert.strictEqual(myVfs.mountPoint, null); - const mountPoint = myVfs.mount('/data'); + const mountPoint = myVfs.mount(); assert.strictEqual(myVfs.mounted, true); assert.strictEqual(myVfs.mountPoint, mountPoint); assert.strictEqual( mountPoint, - path.join(os.devNull, 'vfs', `layer-${myVfs.layerId}`, 'data')); + path.join(os.devNull, 'vfs', String(myVfs.layerId))); myVfs.unmount(); assert.strictEqual(myVfs.mounted, false); assert.strictEqual(myVfs.mountPoint, null); } -// Test: prefix is optional and never resolved against the working -// directory; relative and absolute prefixes are logical names inside -// the reserved namespace. -{ - const myVfs = vfs.create(); - const mountPoint = myVfs.mount(); - assert.strictEqual( - mountPoint, - path.join(os.devNull, 'vfs', `layer-${myVfs.layerId}`)); - myVfs.unmount(); - - const relative = myVfs.mount('deps'); - assert.strictEqual( - relative, - path.join(os.devNull, 'vfs', `layer-${myVfs.layerId}`, 'deps')); - myVfs.unmount(); -} - // Test: double-mount throws { const myVfs = vfs.create(); - myVfs.mount('/data'); - assert.throws(() => myVfs.mount('/data'), { code: 'ERR_INVALID_STATE' }); + myVfs.mount(); + assert.throws(() => myVfs.mount(), { code: 'ERR_INVALID_STATE' }); myVfs.unmount(); } -// Test: prefix cannot escape the reserved namespace; non-string throws -{ - const myVfs = vfs.create(); - assert.throws(() => myVfs.mount('../../escape'), - { code: 'ERR_INVALID_ARG_VALUE' }); - assert.throws(() => myVfs.mount(42), { code: 'ERR_INVALID_ARG_TYPE' }); - assert.strictEqual(myVfs.mounted, false); -} - -// Test: two instances can use the same logical prefix - each gets a -// distinct mount point in its own layer namespace. +// Test: two instances each get a distinct mount point in their own +// layer namespace. { const a = vfs.create(); const b = vfs.create(); a.writeFileSync('/f.txt', 'A'); b.writeFileSync('/f.txt', 'B'); - const mountA = a.mount('/same'); - const mountB = b.mount('/same'); + const mountA = a.mount(); + const mountB = b.mount(); assert.notStrictEqual(mountA, mountB); + assert.strictEqual( + mountA, + path.join(os.devNull, 'vfs', String(a.layerId))); + assert.strictEqual( + mountB, + path.join(os.devNull, 'vfs', String(b.layerId))); assert.strictEqual(fs.readFileSync(path.join(mountA, 'f.txt'), 'utf8'), 'A'); assert.strictEqual(fs.readFileSync(path.join(mountB, 'f.txt'), 'utf8'), 'B'); a.unmount(); @@ -195,7 +174,7 @@ function createMountedVfs() { // Test: Symbol.dispose unmounts { const myVfs = vfs.create(); - myVfs.mount('/data'); + myVfs.mount(); assert.strictEqual(myVfs.mounted, true); myVfs[Symbol.dispose](); assert.strictEqual(myVfs.mounted, false); diff --git a/test/parallel/test-vfs-multi-mount.js b/test/parallel/test-vfs-multi-mount.js index c52d11239b0a5b..7539c0a6f02825 100644 --- a/test/parallel/test-vfs-multi-mount.js +++ b/test/parallel/test-vfs-multi-mount.js @@ -21,8 +21,8 @@ b.writeFileSync('/file.txt', 'from-b'); b.mkdirSync('/dir', { recursive: true }); b.writeFileSync('/dir/inside.txt', 'b-inside'); -const baseA = a.mount('/multi'); -const baseB = b.mount('/multi'); +const baseA = a.mount(); +const baseB = b.mount(); // Each mount sees its own content assert.strictEqual(fs.readFileSync(path.join(baseA, 'file.txt'), 'utf8'), diff --git a/test/parallel/test-vfs-package-json-cache.js b/test/parallel/test-vfs-package-json-cache.js index 1d2dbc41cb1b13..90c2d853c43a9b 100644 --- a/test/parallel/test-vfs-package-json-cache.js +++ b/test/parallel/test-vfs-package-json-cache.js @@ -12,7 +12,7 @@ const myVfs = vfs.create(); myVfs.mkdirSync('/pkg'); myVfs.writeFileSync('/pkg/package.json', '{"name":"test","type":"module"}'); myVfs.writeFileSync('/pkg/index.js', 'module.exports = 42'); -const mountPoint = myVfs.mount('/mnt_pjcache'); +const mountPoint = myVfs.mount(); // Access the file so caches are populated assert.ok(fs.existsSync(`${mountPoint}/pkg/package.json`)); diff --git a/test/parallel/test-vfs-package-json.js b/test/parallel/test-vfs-package-json.js index 367fbad394f400..5bcf7a366e8380 100644 --- a/test/parallel/test-vfs-package-json.js +++ b/test/parallel/test-vfs-package-json.js @@ -16,7 +16,7 @@ const vfs = require('node:vfs'); main: './index.js', exports: { '.': './index.js' }, })); - const mountPoint = myVfs.mount('/mnt/read-test'); + const mountPoint = myVfs.mount(); const packageJsonReader = require('internal/modules/package_json_reader'); const result = packageJsonReader.read( @@ -51,7 +51,7 @@ const vfs = require('node:vfs'); type: 'module', })); myVfs.writeFileSync('/app/src/lib/deep/module.js', ''); - const mountPoint = myVfs.mount('/mnt/parent-test'); + const mountPoint = myVfs.mount(); const packageJsonReader = require('internal/modules/package_json_reader'); const result = packageJsonReader.getNearestParentPackageJSON( @@ -80,7 +80,7 @@ const vfs = require('node:vfs'); exports: { '.': './main.js' }, })); myVfs.writeFileSync('/project/src/index.js', ''); - const mountPoint = myVfs.mount('/mnt/scope-test'); + const mountPoint = myVfs.mount(); const packageJsonReader = require('internal/modules/package_json_reader'); const { pathToFileURL } = require('url'); @@ -101,7 +101,7 @@ const vfs = require('node:vfs'); const myVfs2 = vfs.create(); myVfs2.mkdirSync('/empty/src', { recursive: true }); myVfs2.writeFileSync('/empty/src/file.js', ''); - const mountPoint2 = myVfs2.mount('/mnt/scope-empty'); + const mountPoint2 = myVfs2.mount(); const emptyUrl = pathToFileURL( path.join(mountPoint2, 'empty', 'src', 'file.js'), @@ -121,7 +121,7 @@ const vfs = require('node:vfs'); type: 'module', })); myVfs.writeFileSync('/esm-app/index.js', ''); - const mountPoint = myVfs.mount('/mnt/type-test'); + const mountPoint = myVfs.mount(); const packageJsonReader = require('internal/modules/package_json_reader'); const { pathToFileURL } = require('url'); @@ -135,7 +135,7 @@ const vfs = require('node:vfs'); const myVfs2 = vfs.create(); myVfs2.mkdirSync('/bare', { recursive: true }); myVfs2.writeFileSync('/bare/file.js', ''); - const mountPoint2 = myVfs2.mount('/mnt/type-empty'); + const mountPoint2 = myVfs2.mount(); const noneUrl = pathToFileURL( path.join(mountPoint2, 'bare', 'file.js'), @@ -157,7 +157,7 @@ const vfs = require('node:vfs'); })); myVfs.writeFileSync('/cjs-app/main.js', 'module.exports = { format: "cjs", ok: true };'); - const mountPoint = myVfs.mount('/mnt/e2e-cjs'); + const mountPoint = myVfs.mount(); const result = require(`${mountPoint}/cjs-app/main.js`); assert.strictEqual(result.format, 'cjs'); @@ -174,7 +174,7 @@ const vfs = require('node:vfs'); type: 'module', })); myVfs.writeFileSync('/esm/mod.mjs', 'export const x = 42;'); - const mountPoint = myVfs.mount('/mnt/e2e-esm'); + const mountPoint = myVfs.mount(); // Use .mjs to ensure ESM treatment regardless of package type const mod = require(`${mountPoint}/esm/mod.mjs`); diff --git a/test/parallel/test-vfs-require.js b/test/parallel/test-vfs-require.js index cb5b8cc7e957a1..002ae18f2ebab0 100644 --- a/test/parallel/test-vfs-require.js +++ b/test/parallel/test-vfs-require.js @@ -17,7 +17,7 @@ const vfsImport = (path) => pathToFileURL(path).href; { const myVfs = vfs.create(); myVfs.writeFileSync('/hello.js', 'module.exports = "hello from vfs";'); - const mountPoint = myVfs.mount('/virtual'); + const mountPoint = myVfs.mount(); const result = require(`${mountPoint}/hello.js`); assert.strictEqual(result, 'hello from vfs'); @@ -35,7 +35,7 @@ const vfsImport = (path) => pathToFileURL(path).href; getValue: function() { return 42; } }; `); - const mountPoint = myVfs.mount('/virtual2'); + const mountPoint = myVfs.mount(); const config = require(`${mountPoint}/config.js`); assert.strictEqual(config.name, 'test-config'); @@ -49,7 +49,7 @@ const vfsImport = (path) => pathToFileURL(path).href; // Mount first so the embedded absolute specifier can use the mount point. { const myVfs = vfs.create(); - const mountPoint = myVfs.mount('/virtual3'); + const mountPoint = myVfs.mount(); myVfs.writeFileSync(`${mountPoint}/utils.js`, ` module.exports = { add: function(a, b) { return a + b; } @@ -75,7 +75,7 @@ const vfsImport = (path) => pathToFileURL(path).href; items: [1, 2, 3], enabled: true, })); - const mountPoint = myVfs.mount('/virtual4'); + const mountPoint = myVfs.mount(); const data = require(`${mountPoint}/data.json`); assert.deepStrictEqual(data.items, [1, 2, 3]); @@ -95,7 +95,7 @@ const vfsImport = (path) => pathToFileURL(path).href; myVfs.writeFileSync('/my-package/index.js', ` module.exports = { loaded: true }; `); - const mountPoint = myVfs.mount('/virtual5'); + const mountPoint = myVfs.mount(); const pkg = require(`${mountPoint}/my-package`); assert.strictEqual(pkg.loaded, true); @@ -107,7 +107,7 @@ const vfsImport = (path) => pathToFileURL(path).href; { const myVfs = vfs.create(); myVfs.writeFileSync('/test.js', 'module.exports = 1;'); - myVfs.mount('/virtual6'); + myVfs.mount(); // require('assert') should still work (builtin) assert.strictEqual(typeof assert.strictEqual, 'function'); @@ -130,7 +130,7 @@ const vfsImport = (path) => pathToFileURL(path).href; const helper = require('./helper.js'); module.exports = helper.help(); `); - const mountPoint = myVfs.mount('/virtual8'); + const mountPoint = myVfs.mount(); const result = require(`${mountPoint}/lib/index.js`); assert.strictEqual(result, 'helped'); @@ -142,7 +142,7 @@ const vfsImport = (path) => pathToFileURL(path).href; { const myVfs = vfs.create(); myVfs.writeFileSync('/file.txt', 'virtual content'); - const mountPoint = myVfs.mount('/virtual9'); + const mountPoint = myVfs.mount(); const content = fs.readFileSync(`${mountPoint}/file.txt`, 'utf8'); assert.strictEqual(content, 'virtual content'); @@ -154,7 +154,7 @@ const vfsImport = (path) => pathToFileURL(path).href; { const myVfs = vfs.create(); myVfs.writeFileSync('/esm.mjs', 'export const msg = "hello from esm";'); - const mountPoint = myVfs.mount('/virtual11'); + const mountPoint = myVfs.mount(); const mod = require(`${mountPoint}/esm.mjs`); assert.strictEqual(mod.msg, 'hello from esm'); @@ -166,7 +166,7 @@ const vfsImport = (path) => pathToFileURL(path).href; { const myVfs = vfs.create(); myVfs.writeFileSync('/esm-default.mjs', 'export default function() { return 42; }'); - const mountPoint = myVfs.mount('/virtual12'); + const mountPoint = myVfs.mount(); const mod = require(`${mountPoint}/esm-default.mjs`); assert.strictEqual(mod.default(), 42); @@ -185,7 +185,7 @@ const vfsImport = (path) => pathToFileURL(path).href; myVfs.writeFileSync('/app/lib.js', 'export const value = 42;' + ' export function hello() { return "hi"; }'); - const mountPoint = myVfs.mount('/virtual13'); + const mountPoint = myVfs.mount(); const mod = require(`${mountPoint}/app/lib.js`); assert.strictEqual(mod.value, 42); @@ -205,7 +205,7 @@ const vfsImport = (path) => pathToFileURL(path).href; myVfs.writeFileSync('/project/src/utils/math.js', 'export const add = (a, b) => a + b;' + ' export default 99;'); - const mountPoint = myVfs.mount('/virtual14'); + const mountPoint = myVfs.mount(); const mod = require(`${mountPoint}/project/src/utils/math.js`); assert.strictEqual(mod.add(3, 4), 7); @@ -223,7 +223,7 @@ const vfsImport = (path) => pathToFileURL(path).href; })); myVfs.writeFileSync('/cjs-app/index.js', 'module.exports = { cjs: true };'); - const mountPoint = myVfs.mount('/virtual15'); + const mountPoint = myVfs.mount(); const mod = require(`${mountPoint}/cjs-app/index.js`); assert.strictEqual(mod.cjs, true); @@ -242,7 +242,7 @@ const vfsImport = (path) => pathToFileURL(path).href; myVfs.writeFileSync('/multi/src/main.js', 'import { X } from "./dep.js";' + ' export const result = X + 1;'); - const mountPoint = myVfs.mount('/virtual16'); + const mountPoint = myVfs.mount(); const mod = require(`${mountPoint}/multi/src/main.js`); assert.strictEqual(mod.result, 101); @@ -255,7 +255,7 @@ const vfsImport = (path) => pathToFileURL(path).href; const myVfs = vfs.create(); myVfs.writeFileSync('/no-pkg.mjs', 'export const x = 1; export default "hello";'); - const mountPoint = myVfs.mount('/virtual17'); + const mountPoint = myVfs.mount(); const mod = require(`${mountPoint}/no-pkg.mjs`); assert.strictEqual(mod.x, 1); @@ -271,7 +271,7 @@ const vfsImport = (path) => pathToFileURL(path).href; myVfs.writeFileSync('/app/package.json', JSON.stringify({ name: 'no-type' })); myVfs.writeFileSync('/app/lib.mjs', 'export const val = 42;'); - const mountPoint = myVfs.mount('/virtual18'); + const mountPoint = myVfs.mount(); const mod = require(`${mountPoint}/app/lib.mjs`); assert.strictEqual(mod.val, 42); @@ -288,7 +288,7 @@ const vfsImport = (path) => pathToFileURL(path).href; type: 'commonjs', })); myVfs.writeFileSync('/cjs-pkg/esm.mjs', 'export const z = 99;'); - const mountPoint = myVfs.mount('/virtual19'); + const mountPoint = myVfs.mount(); const mod = require(`${mountPoint}/cjs-pkg/esm.mjs`); assert.strictEqual(mod.z, 99); @@ -305,7 +305,7 @@ const vfsImport = (path) => pathToFileURL(path).href; main: './lib/entry', })); myVfs.writeFileSync('/pkg/lib/entry.js', 'module.exports = "legacy-main";'); - const mountPoint = myVfs.mount('/virtual20'); + const mountPoint = myVfs.mount(); const result = require(`${mountPoint}/pkg`); assert.strictEqual(result, 'legacy-main'); @@ -321,7 +321,7 @@ const vfsImport = (path) => pathToFileURL(path).href; name: 'no-main-pkg', })); myVfs.writeFileSync('/pkg2/index.js', 'module.exports = "index-fallback";'); - const mountPoint = myVfs.mount('/virtual21'); + const mountPoint = myVfs.mount(); const result = require(`${mountPoint}/pkg2`); assert.strictEqual(result, 'index-fallback'); @@ -343,7 +343,7 @@ const vfsImport = (path) => pathToFileURL(path).href; 'export const value = "esm-legacy-main";'); myVfs.writeFileSync('/app/main.mjs', 'export { value } from "esm-legacy-main";'); - const mountPoint = myVfs.mount('/virtual20b'); + const mountPoint = myVfs.mount(); import(vfsImport(`${mountPoint}/app/main.mjs`)).then(common.mustCall((mod) => { assert.strictEqual(mod.value, 'esm-legacy-main'); @@ -363,7 +363,7 @@ const vfsImport = (path) => pathToFileURL(path).href; 'export const value = "esm-index-fallback";'); myVfs.writeFileSync('/app2/main.mjs', 'export { value } from "esm-nomain";'); - const mountPoint = myVfs.mount('/virtual21b'); + const mountPoint = myVfs.mount(); import(vfsImport(`${mountPoint}/app2/main.mjs`)).then(common.mustCall((mod) => { assert.strictEqual(mod.value, 'esm-index-fallback'); @@ -380,7 +380,7 @@ const vfsImport = (path) => pathToFileURL(path).href; type: 'module', })); myVfs.writeFileSync('/esm-pkg/entry', 'export const x = 123;'); - const mountPoint = myVfs.mount('/virtual22'); + const mountPoint = myVfs.mount(); // Use import() to trigger ESM loader path for extensionless file detection import(vfsImport(`${mountPoint}/esm-pkg/entry`)).then(common.mustCall((mod) => { @@ -393,7 +393,7 @@ const vfsImport = (path) => pathToFileURL(path).href; { const myVfs = vfs.create(); myVfs.writeFileSync('/unmount-test.js', 'module.exports = "before unmount";'); - const mountPoint = myVfs.mount('/virtual10'); + const mountPoint = myVfs.mount(); const result = require(`${mountPoint}/unmount-test.js`); assert.strictEqual(result, 'before unmount'); diff --git a/test/parallel/test-vfs-scoped-cache-purge.js b/test/parallel/test-vfs-scoped-cache-purge.js index 4627e532a148bc..cc1b19c16be295 100644 --- a/test/parallel/test-vfs-scoped-cache-purge.js +++ b/test/parallel/test-vfs-scoped-cache-purge.js @@ -18,11 +18,11 @@ const vfsImport = (path) => pathToFileURL(path).href; { const a = vfs.create(); a.writeFileSync('/value.js', 'module.exports = "a-cached"'); - const mountA = a.mount('/mnt-purge'); + const mountA = a.mount(); const b = vfs.create(); b.writeFileSync('/value.js', 'module.exports = "b-cached"'); - const mountB = b.mount('/mnt-purge'); + const mountB = b.mount(); // Warm both caches. assert.strictEqual(require(`${mountA}/value.js`), 'a-cached'); @@ -52,7 +52,7 @@ const vfsImport = (path) => pathToFileURL(path).href; v.writeFileSync('/m.mjs', 'export const url = import.meta.url;\n' + 'export const resolved = import.meta.resolve("./m.mjs");'); - const mountPoint = v.mount('/mnt-url'); + const mountPoint = v.mount(); const ns = await import(vfsImport(`${mountPoint}/m.mjs`)); assert.strictEqual(ns.url, pathToFileURL(`${mountPoint}/m.mjs`).href); From 023516ee2a90bed2d99a4601805a363d4629172e Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 13 Jul 2026 02:15:23 -0700 Subject: [PATCH 14/31] Update lib/internal/vfs/setup.js Co-authored-by: Joyee Cheung --- lib/internal/vfs/setup.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/internal/vfs/setup.js b/lib/internal/vfs/setup.js index dc696d96c0db2e..836539f3532edd 100644 --- a/lib/internal/vfs/setup.js +++ b/lib/internal/vfs/setup.js @@ -231,13 +231,15 @@ function findVFSForStat(filename) { function findVFSForRead(filename, options) { const vfs = findVFS(filename); if (vfs === null) return null; - if (vfs.existsSync(filename) && vfsStat(vfs, filename) === 0) { + try { return { vfs, content: vfs.readFileSync(filename, options) }; + } catch (e) { + const code = e?.code; + if (code === 'ENOENT' || code === 'EISDIR') { + throw createENOENT('open', filename); + } + throw e; } - // Path inside mount but missing/not-a-file: synthesize ENOENT. Paths - // under the reserved namespace never exist on the real file system, - // so falling through would produce a confusing real-fs error. - throw createENOENT('open', filename); } /** From e3784356e0b3e1d96ba1ecf0ebecc04a7994ac95 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 13 Jul 2026 02:16:06 -0700 Subject: [PATCH 15/31] Update lib/internal/vfs/setup.js Co-authored-by: Joyee Cheung --- lib/internal/vfs/setup.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/internal/vfs/setup.js b/lib/internal/vfs/setup.js index 836539f3532edd..99a1ef4b5fe040 100644 --- a/lib/internal/vfs/setup.js +++ b/lib/internal/vfs/setup.js @@ -206,8 +206,9 @@ function vfsStat(vfs, filePath) { const stats = vfs.statSync(filePath); if (stats.isDirectory()) return 1; return 0; - } catch { - return -2; + } catch (err) { + if (typeof err?.errno === 'number') return err.errno; + throw err; } } From 7e94c3d09dc5bce63d8f43c95dc26efca4844821 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 13 Jul 2026 11:23:56 +0200 Subject: [PATCH 16/31] vfs: drop unused shouldHandle / router exports `VirtualFileSystem#shouldHandle` had no remaining callers: dispatch goes through `shouldHandleNormalized` and every code path outside dispatch was rewritten to `findVFS` after the reserved-namespace refactor. Two router.js exports were also unreferenced: `getVfsRoot` (used only internally by `getNormalizedVfsRoot` and `getLayerRoot`) and the re-export of `path.isAbsolute` as `isAbsolutePath`. The corresponding test-vfs-router assertions and the `isAbsolute` local binding on the `require('path')` destructure go with them. Signed-off-by: Matteo Collina --- lib/internal/vfs/file_system.js | 27 ++++++++------------------- lib/internal/vfs/router.js | 4 +--- test/parallel/test-vfs-router.js | 6 +----- 3 files changed, 10 insertions(+), 27 deletions(-) diff --git a/lib/internal/vfs/file_system.js b/lib/internal/vfs/file_system.js index 766a7ea00cdbbd..7a05472688c7d3 100644 --- a/lib/internal/vfs/file_system.js +++ b/lib/internal/vfs/file_system.js @@ -43,8 +43,8 @@ let debug = require('internal/util/debuglog').debuglog('vfs', (fn) => { const kProvider = Symbol('kProvider'); const kMountPoint = Symbol('kMountPoint'); // Cached `normalizeMountedPath(kMountPoint)` so the hot dispatch path -// (shouldHandle) does not recompute toNamespacedPath(resolve(...)) on -// every fs call. Set at mount time, cleared at unmount. +// does not recompute toNamespacedPath(resolve(...)) on every fs call. +// Set at mount time, cleared at unmount. const kNormalizedMountPoint = Symbol('kNormalizedMountPoint'); const kMounted = Symbol('kMounted'); const kPromises = Symbol('kPromises'); @@ -214,23 +214,12 @@ class VirtualFileSystem { } /** - * Checks if a path should be handled by this VFS. - * @param {string} inputPath The path to check (must be absolute & normalized) - * @returns {boolean} - */ - shouldHandle(inputPath) { - const mountPoint = this[kNormalizedMountPoint]; - if (mountPoint === null) { - return false; - } - return isUnderMountPoint(normalizeMountedPath(inputPath), mountPoint); - } - - /** - * Fast path used by the VFS dispatch, which normalizes the input - * path once and reuses the result. - * @param {string} normalizedInput A path already passed through - * normalizeMountedPath. + * Returns true when `normalizedInput` falls inside this VFS's + * reserved mount point. The caller must have already passed the + * input through `normalizeMountedPath`; the dispatch loop in + * `internal/vfs/setup.js` normalizes once and hands the same value + * to every candidate layer. + * @param {string} normalizedInput * @returns {boolean} */ shouldHandleNormalized(normalizedInput) { diff --git a/lib/internal/vfs/router.js b/lib/internal/vfs/router.js index 0a47836f69021f..4b28ce71533fb1 100644 --- a/lib/internal/vfs/router.js +++ b/lib/internal/vfs/router.js @@ -7,7 +7,7 @@ const { StringPrototypeStartsWith, } = primordials; -const { isAbsolute, relative, resolve, sep, toNamespacedPath } = require('path'); +const { relative, resolve, sep, toNamespacedPath } = require('path'); // All VFS mount points live under `${os.devNull}/vfs//`. // `os.devNull` is a character device on POSIX (`/dev/null`) and a @@ -101,8 +101,6 @@ module.exports = { getLayerIdFromPath, getLayerRoot, getNormalizedVfsRoot, - getVfsRoot, isUnderMountPoint, getRelativePath, - isAbsolutePath: isAbsolute, }; diff --git a/test/parallel/test-vfs-router.js b/test/parallel/test-vfs-router.js index 864a684f5c2df4..bb2656dee30e0c 100644 --- a/test/parallel/test-vfs-router.js +++ b/test/parallel/test-vfs-router.js @@ -9,7 +9,7 @@ require('../common'); const assert = require('assert'); const path = require('path'); -const { isUnderMountPoint, getRelativePath, isAbsolutePath } = +const { isUnderMountPoint, getRelativePath } = require('internal/vfs/router'); const mount = path.resolve('/app'); @@ -57,7 +57,3 @@ if (process.platform !== 'win32') { const mountA = path.resolve('/m/a'); const deep = path.join(mountA, 'b', 'c', 'd'); assert.strictEqual(getRelativePath(deep, mountA), '/b/c/d'); - -// isAbsolutePath is re-exported from node:path -assert.strictEqual(isAbsolutePath(path.resolve('/foo')), true); -assert.strictEqual(isAbsolutePath('foo'), false); From 2203b593dae98e0221f0fd66fbee630fcc0b6b52 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 13 Jul 2026 11:32:41 +0200 Subject: [PATCH 17/31] vfs: use internal EXTENSIONLESS_FORMAT_* constants Signed-off-by: Matteo Collina --- lib/internal/vfs/setup.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/internal/vfs/setup.js b/lib/internal/vfs/setup.js index 99a1ef4b5fe040..bedbc7f7b6430a 100644 --- a/lib/internal/vfs/setup.js +++ b/lib/internal/vfs/setup.js @@ -874,6 +874,7 @@ function installModuleLoaderOverrides() { } = require('internal/modules/helpers'); const internalFsBinding = internalBinding('fs'); const nativeModulesBinding = internalBinding('modules'); + const { internal: internalConstants } = internalBinding('constants'); setLoaderFsOverrides({ stat(filename) { @@ -932,7 +933,7 @@ function installModuleLoaderOverrides() { // mount. Treat that as JS (the caller will surface the real // error when it later tries to load source). Propagate every // other code (EACCES, ELOOP, etc). - if (e?.code === 'ENOENT') return 0; // EXTENSIONLESS_FORMAT_JAVASCRIPT + if (e?.code === 'ENOENT') return internalConstants.EXTENSIONLESS_FORMAT_JAVASCRIPT; throw e; } if (result === null) return undefined; @@ -941,9 +942,9 @@ function installModuleLoaderOverrides() { if (content && content.length >= 4 && content[0] === 0x00 && content[1] === 0x61 && content[2] === 0x73 && content[3] === 0x6d) { - return 1; // EXTENSIONLESS_FORMAT_WASM + return internalConstants.EXTENSIONLESS_FORMAT_WASM; } - return 0; // EXTENSIONLESS_FORMAT_JAVASCRIPT + return internalConstants.EXTENSIONLESS_FORMAT_JAVASCRIPT; }, }); From f2f7b04988f55122859e63bd233a9cd2ffefccf0 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 13 Jul 2026 11:35:18 +0200 Subject: [PATCH 18/31] vfs: raise ERR_INVALID_PACKAGE_CONFIG for CJS too Signed-off-by: Matteo Collina --- lib/internal/vfs/setup.js | 9 +++------ test/parallel/test-vfs-invalid-package-json.js | 8 +++++--- test/parallel/test-vfs-module-hooks.mjs | 7 ++++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/internal/vfs/setup.js b/lib/internal/vfs/setup.js index bedbc7f7b6430a..a6dce38bf121c9 100644 --- a/lib/internal/vfs/setup.js +++ b/lib/internal/vfs/setup.js @@ -966,12 +966,9 @@ function installModuleLoaderOverrides() { try { parsed = JSONParse(content); } catch (err) { - // ESM raises ERR_INVALID_PACKAGE_CONFIG on malformed JSON; - // CJS silently ignores it (legacy behavior). - if (isESM) { - throw new ERR_INVALID_PACKAGE_CONFIG(jsonPath, base, err.message); - } - return undefined; + // Both CJS and ESM raise ERR_INVALID_PACKAGE_CONFIG on + // malformed JSON (see nodejs/node#48606). + throw new ERR_INVALID_PACKAGE_CONFIG(jsonPath, base, err.message); } // serializePackageJSON may throw ERR_INVALID_PACKAGE_CONFIG for // wrong-type fields - intentionally not caught. diff --git a/test/parallel/test-vfs-invalid-package-json.js b/test/parallel/test-vfs-invalid-package-json.js index 2c87eb5d5e4cd7..0b07f6979e5092 100644 --- a/test/parallel/test-vfs-invalid-package-json.js +++ b/test/parallel/test-vfs-invalid-package-json.js @@ -5,8 +5,9 @@ require('../common'); const assert = require('assert'); const vfs = require('node:vfs'); -// Test that invalid package.json in VFS falls through to index.js -// (bad JSON is skipped, like a missing package.json). +// Test that a malformed package.json in VFS throws +// ERR_INVALID_PACKAGE_CONFIG (matches native CJS behavior after +// nodejs/node#48606). { const myVfs = vfs.create(); myVfs.mkdirSync('/pkg', { recursive: true }); @@ -14,7 +15,8 @@ const vfs = require('node:vfs'); myVfs.writeFileSync('/pkg/index.js', 'module.exports = 42;'); const mountPoint = myVfs.mount(); - assert.strictEqual(require(`${mountPoint}/pkg`), 42); + assert.throws(() => require(`${mountPoint}/pkg`), + { code: 'ERR_INVALID_PACKAGE_CONFIG' }); myVfs.unmount(); } diff --git a/test/parallel/test-vfs-module-hooks.mjs b/test/parallel/test-vfs-module-hooks.mjs index 833333d144941e..533cda749a8592 100644 --- a/test/parallel/test-vfs-module-hooks.mjs +++ b/test/parallel/test-vfs-module-hooks.mjs @@ -639,9 +639,10 @@ const vfsImport = (path) => pathToFileURL(path).href; ); const mountPoint = myVfs.mount(); - // Should fall through to index.js after failing to parse package.json - const result = require(`${mountPoint}/bad-json-dir`); - assert.strictEqual(result.fallbackIndex, true); + // A malformed package.json raises ERR_INVALID_PACKAGE_CONFIG for + // both CJS and ESM after nodejs/node#48606. + assert.throws(() => require(`${mountPoint}/bad-json-dir`), + { code: 'ERR_INVALID_PACKAGE_CONFIG' }); myVfs.unmount(); } From 2462d17e32b8d8f1b1f0c6bf335eb3254f105fe0 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 13 Jul 2026 11:36:31 +0200 Subject: [PATCH 19/31] vfs: default all read errors in getFormatOfExtensionlessFile to JS Signed-off-by: Matteo Collina --- lib/internal/vfs/setup.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/internal/vfs/setup.js b/lib/internal/vfs/setup.js index a6dce38bf121c9..6e6a4c24310a54 100644 --- a/lib/internal/vfs/setup.js +++ b/lib/internal/vfs/setup.js @@ -928,13 +928,11 @@ function installModuleLoaderOverrides() { let result; try { result = findVFSForRead(filePath, null); - } catch (e) { - // findVFSForRead synthesizes ENOENT for missing paths inside a - // mount. Treat that as JS (the caller will surface the real - // error when it later tries to load source). Propagate every - // other code (EACCES, ELOOP, etc). - if (e?.code === 'ENOENT') return internalConstants.EXTENSIONLESS_FORMAT_JAVASCRIPT; - throw e; + } catch { + // Match the C++ binding: any read error means we cannot sniff + // the magic bytes, so default to JavaScript. The caller will + // surface the real error when it later tries to load source. + return internalConstants.EXTENSIONLESS_FORMAT_JAVASCRIPT; } if (result === null) return undefined; const content = result.content; From 902254d8bfceb5b4be4dfc27325f8aada602054a Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 13 Jul 2026 11:41:51 +0200 Subject: [PATCH 20/31] vfs: return normalized path from findVFS Signed-off-by: Matteo Collina --- lib/internal/vfs/setup.js | 114 +++++++++++++++++++++----------------- 1 file changed, 62 insertions(+), 52 deletions(-) diff --git a/lib/internal/vfs/setup.js b/lib/internal/vfs/setup.js index 6e6a4c24310a54..d219ab353489a9 100644 --- a/lib/internal/vfs/setup.js +++ b/lib/internal/vfs/setup.js @@ -128,11 +128,12 @@ function deregisterVFS(vfs) { /** * Resolves a path string to the active VFS that owns it, or null. * Ownership is decidable from the path alone: all mount points live - * under the reserved `${os.devNull}/vfs/` namespace, - * so a single prefix comparison rejects every real-file-system path - * and a map lookup finds the owning layer. + * under the reserved `${os.devNull}/vfs/` namespace, so a single + * prefix comparison rejects every real-file-system path and a map + * lookup finds the owning layer. The normalized path is returned + * alongside the layer so downstream helpers can skip renormalization. * @param {string} inputPath - * @returns {object|null} The owning VirtualFileSystem or null + * @returns {{ vfs: object, normalized: string }|null} */ function findVFS(inputPath) { const normalized = normalizeMountedPath(inputPath); @@ -145,7 +146,7 @@ function findVFS(inputPath) { if (vfs === undefined || !vfs.shouldHandleNormalized(normalized)) { return null; } - return vfs; + return { vfs, normalized }; } /** @@ -218,9 +219,9 @@ function vfsStat(vfs, filePath) { * @returns {{ vfs: object, result: number }|null} */ function findVFSForStat(filename) { - const vfs = findVFS(filename); - if (vfs === null) return null; - return { vfs, result: vfsStat(vfs, filename) }; + const r = findVFS(filename); + if (r === null) return null; + return { vfs: r.vfs, result: vfsStat(r.vfs, filename) }; } /** @@ -230,10 +231,10 @@ function findVFSForStat(filename) { * @returns {{ vfs: object, content: Buffer|string }|null} */ function findVFSForRead(filename, options) { - const vfs = findVFS(filename); - if (vfs === null) return null; + const r = findVFS(filename); + if (r === null) return null; try { - return { vfs, content: vfs.readFileSync(filename, options) }; + return { vfs: r.vfs, content: r.vfs.readFileSync(filename, options) }; } catch (e) { const code = e?.code; if (code === 'ENOENT' || code === 'EISDIR') { @@ -315,12 +316,19 @@ function serializePackageJSON(parsed, filePath) { * node_modules) - used as the "not found" marker matching the C++ * binding's contract for getPackageScopeConfig. * @param {object} vfs The VFS that owns startPath - * @param {string} startPath Absolute path to start from + * @param {string} startPath Absolute path to start from - the result + * returned to the caller (e.g. sentinel) is built from this, so that + * error messages and cache keys keep the caller's original form. + * @param {string} normalizedStart Same as startPath but passed through + * `normalizeMountedPath`. Used to walk the directory chain so the + * containment check is a plain string prefix rather than a fresh + * `toNamespacedPath(resolve(...))` on every iteration. * @returns {{ vfs?: object, pjsonPath?: string, parsed?: object, sentinel: string }} */ -function findVFSPackageJSON(vfs, startPath) { +function findVFSPackageJSON(vfs, startPath, normalizedStart) { const normalizedMount = normalizeMountedPath(vfs.mountPoint); let currentDir = dirname(startPath); + let currentNorm = dirname(normalizedStart); let lastDir; let sentinel = join(currentDir, 'package.json'); while (currentDir !== lastDir) { @@ -328,13 +336,13 @@ function findVFSPackageJSON(vfs, startPath) { StringPrototypeEndsWith(currentDir, '\\node_modules')) { break; } - const pjsonPath = join(currentDir, 'package.json'); - if (!isUnderMountPoint(normalizeMountedPath(pjsonPath), normalizedMount)) { + if (!isUnderMountPoint(currentNorm, normalizedMount)) { // Walked above the mount point: nothing between the mount point // and the file-system root can exist (the namespace lives under // os.devNull, a device that cannot have children). break; } + const pjsonPath = join(currentDir, 'package.json'); sentinel = pjsonPath; if (vfsStat(vfs, pjsonPath) === 0) { try { @@ -347,28 +355,29 @@ function findVFSPackageJSON(vfs, startPath) { } lastDir = currentDir; currentDir = dirname(currentDir); + currentNorm = dirname(currentNorm); } return { sentinel }; } function findVFSForExists(filename) { - const vfs = findVFS(filename); - if (vfs === null) return null; - return { vfs, exists: vfs.existsSync(filename) }; + const r = findVFS(filename); + if (r === null) return null; + return { vfs: r.vfs, exists: r.vfs.existsSync(filename) }; } function findVFSForPath(filename) { - const vfs = findVFS(filename); - if (vfs === null) return null; - return { vfs, path: filename }; + const r = findVFS(filename); + if (r === null) return null; + return { vfs: r.vfs, path: filename }; } // Sync read: check exists first, fall through to ENOENT for mounted VFS. function findVFSWith(filename, syscall, fn) { - const vfs = findVFS(filename); - if (vfs === null) return undefined; - if (vfs.existsSync(filename)) { - return fn(vfs, filename); + const r = findVFS(filename); + if (r === null) return undefined; + if (r.vfs.existsSync(filename)) { + return fn(r.vfs, filename); } throw createENOENT(syscall, filename); } @@ -398,7 +407,7 @@ function vfsOpVoid(path, fn) { } function checkSameVFS(srcPath, destPath, syscall, srcVfs) { - if (findVFS(destPath) !== srcVfs) { + if (findVFS(destPath)?.vfs !== srcVfs) { throw createEXDEV(syscall, srcPath); } } @@ -443,11 +452,11 @@ function createVfsHandlers() { lstatSync(path, options) { const pathStr = toPathStr(path); if (pathStr === null) return undefined; - const vfs = findVFS(pathStr); - if (vfs === null) return undefined; + const r = findVFS(pathStr); + if (r === null) return undefined; // Missing entries surface as ENOENT so fs.lstatSync's own // throwIfNoEntry handling can apply uniformly. - return vfs.lstatSync(pathStr, options); + return r.vfs.lstatSync(pathStr, options); }, statSync(path, options) { return vfsRead(path, 'stat', (vfs, n) => vfs.statSync(n, options)); @@ -476,9 +485,9 @@ function createVfsHandlers() { readlinkSync(path, options) { const pathStr = toPathStr(path); if (pathStr === null) return undefined; - const vfs = findVFS(pathStr); - if (vfs === null) return undefined; - const result = vfs.readlinkSync(pathStr, options); + const r = findVFS(pathStr); + if (r === null) return undefined; + const result = r.vfs.readlinkSync(pathStr, options); if (options?.encoding === 'buffer') return Buffer.from(result); return result; }, @@ -553,9 +562,9 @@ function createVfsHandlers() { openAsBlob(path, options) { const pathStr = toPathStr(path); if (pathStr !== null) { - const vfs = findVFS(pathStr); - if (vfs !== null && vfs.existsSync(pathStr)) { - return vfs.openAsBlob(pathStr, options); + const r = findVFS(pathStr); + if (r !== null && r.vfs.existsSync(pathStr)) { + return r.vfs.openAsBlob(pathStr, options); } } return undefined; @@ -711,9 +720,9 @@ function createVfsHandlers() { lstat(path, options) { const pathStr = toPathStr(path); if (pathStr === null) return undefined; - const vfs = findVFS(pathStr); - if (vfs === null) return undefined; - return vfs.promises.lstat(pathStr, options); + const r = findVFS(pathStr); + if (r === null) return undefined; + return r.vfs.promises.lstat(pathStr, options); }, stat(path, options) { const promise = vfsOp(path, (vfs, n) => vfs.promises.stat(n, options)); @@ -757,9 +766,9 @@ function createVfsHandlers() { readlink(path, options) { const pathStr = toPathStr(path); if (pathStr === null) return undefined; - const vfs = findVFS(pathStr); - if (vfs === null) return undefined; - const promise = vfs.promises.readlink(pathStr, options); + const r = findVFS(pathStr); + if (r === null) return undefined; + const promise = r.vfs.promises.readlink(pathStr, options); if (options?.encoding === 'buffer') { return promise.then((result) => Buffer.from(result)); } @@ -948,10 +957,11 @@ function installModuleLoaderOverrides() { setLoaderPackageOverrides({ readPackageJSON(jsonPath, isESM, base, specifier) { - const vfs = findVFS(jsonPath); - if (vfs === null) { + const r = findVFS(jsonPath); + if (r === null) { return nativeModulesBinding.readPackageJSON(jsonPath, isESM, base, specifier); } + const { vfs } = r; if (vfsStat(vfs, jsonPath) !== 0) return undefined; let content; try { @@ -973,11 +983,11 @@ function installModuleLoaderOverrides() { return serializePackageJSON(parsed, jsonPath); }, getNearestParentPackageJSON(checkPath) { - const vfs = findVFS(checkPath); - if (vfs === null) { + const r = findVFS(checkPath); + if (r === null) { return nativeModulesBinding.getNearestParentPackageJSON(checkPath); } - const found = findVFSPackageJSON(vfs, checkPath); + const found = findVFSPackageJSON(r.vfs, checkPath, r.normalized); if (found.parsed !== undefined) { return serializePackageJSON(found.parsed, found.pjsonPath); } @@ -994,11 +1004,11 @@ function installModuleLoaderOverrides() { } else { filePath = resolved; } - const vfs = findVFS(filePath); - if (vfs === null) { + const r = findVFS(filePath); + if (r === null) { return nativeModulesBinding.getPackageScopeConfig(resolved); } - const found = findVFSPackageJSON(vfs, filePath); + const found = findVFSPackageJSON(r.vfs, filePath, r.normalized); if (found.parsed !== undefined) { return serializePackageJSON(found.parsed, found.pjsonPath); } @@ -1017,11 +1027,11 @@ function installModuleLoaderOverrides() { } else { filePath = url; } - const vfs = findVFS(filePath); - if (vfs === null) { + const r = findVFS(filePath); + if (r === null) { return nativeModulesBinding.getPackageType(url); } - const found = findVFSPackageJSON(vfs, filePath); + const found = findVFSPackageJSON(r.vfs, filePath, r.normalized); if (found.parsed !== undefined) { // Route through serializePackageJSON so a malformed `type` // (non-string) throws ERR_INVALID_PACKAGE_CONFIG, matching From 2716996abbcc99d9ba90459ccebaa016a7db8d8e Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 13 Jul 2026 11:49:08 +0200 Subject: [PATCH 21/31] vfs: inline isUnderMountPoint and use shouldHandleNormalized Signed-off-by: Matteo Collina --- lib/internal/vfs/file_system.js | 13 ++++++------ lib/internal/vfs/router.js | 21 -------------------- lib/internal/vfs/setup.js | 4 +--- test/parallel/test-vfs-router.js | 34 +------------------------------- 4 files changed, 8 insertions(+), 64 deletions(-) diff --git a/lib/internal/vfs/file_system.js b/lib/internal/vfs/file_system.js index 7a05472688c7d3..a8c2ba20eadf04 100644 --- a/lib/internal/vfs/file_system.js +++ b/lib/internal/vfs/file_system.js @@ -3,6 +3,7 @@ const { MathRandom, ObjectFreeze, + StringPrototypeStartsWith, Symbol, SymbolDispose, } = primordials; @@ -15,11 +16,10 @@ const { const { validateBoolean } = require('internal/validators'); const { MemoryProvider } = require('internal/vfs/providers/memory'); const path = require('path'); -const { posix: pathPosix, resolve: resolvePath, toNamespacedPath } = path; +const { posix: pathPosix, resolve: resolvePath, sep, toNamespacedPath } = path; const { join: joinPath } = pathPosix; const { getLayerRoot, - isUnderMountPoint, getRelativePath, } = require('internal/vfs/router'); const { @@ -224,10 +224,9 @@ class VirtualFileSystem { */ shouldHandleNormalized(normalizedInput) { const mountPoint = this[kNormalizedMountPoint]; - if (mountPoint === null) { - return false; - } - return isUnderMountPoint(normalizedInput, mountPoint); + if (mountPoint === null) return false; + if (normalizedInput === mountPoint) return true; + return StringPrototypeStartsWith(normalizedInput, mountPoint + sep); } // ==================== Path Resolution ==================== @@ -242,7 +241,7 @@ class VirtualFileSystem { const mountPoint = this[kNormalizedMountPoint]; if (mountPoint !== null) { const resolved = normalizeMountedPath(inputPath); - if (!isUnderMountPoint(resolved, mountPoint)) { + if (!this.shouldHandleNormalized(resolved)) { throw createENOENT('open', inputPath); } return getRelativePath(resolved, mountPoint); diff --git a/lib/internal/vfs/router.js b/lib/internal/vfs/router.js index 4b28ce71533fb1..e26a93e89b78e3 100644 --- a/lib/internal/vfs/router.js +++ b/lib/internal/vfs/router.js @@ -4,7 +4,6 @@ const { ArrayPrototypeJoin, StringPrototypeCharCodeAt, StringPrototypeSplit, - StringPrototypeStartsWith, } = primordials; const { relative, resolve, sep, toNamespacedPath } = require('path'); @@ -66,22 +65,6 @@ function getLayerIdFromPath(normalizedPath) { return id; } -// `path.sep` is required here because on Windows `path.resolve('/virtual')` -// produces 'C:\virtual' and all resolved paths use backslashes - a hardcoded -// '/' check would never match. The trailing-separator guard handles root -// mount points like 'C:\' so we don't end up with 'C:\\'. -function isUnderMountPoint(normalizedPath, mountPoint) { - if (normalizedPath === mountPoint) { - return true; - } - if (mountPoint === '/') { - return StringPrototypeStartsWith(normalizedPath, '/'); - } - const prefix = mountPoint[mountPoint.length - 1] === sep ? - mountPoint : mountPoint + sep; - return StringPrototypeStartsWith(normalizedPath, prefix); -} - // Returns a POSIX-style relative path the provider can consume. Uses // `path.relative()` so Windows backslash paths are handled correctly, then // re-joins with forward slashes for the provider's internal POSIX format. @@ -89,9 +72,6 @@ function getRelativePath(normalizedPath, mountPoint) { if (normalizedPath === mountPoint) { return '/'; } - if (mountPoint === '/') { - return normalizedPath; - } const rel = relative(mountPoint, normalizedPath); const segments = StringPrototypeSplit(rel, sep); return '/' + ArrayPrototypeJoin(segments, '/'); @@ -101,6 +81,5 @@ module.exports = { getLayerIdFromPath, getLayerRoot, getNormalizedVfsRoot, - isUnderMountPoint, getRelativePath, }; diff --git a/lib/internal/vfs/setup.js b/lib/internal/vfs/setup.js index d219ab353489a9..f663ec40cb806e 100644 --- a/lib/internal/vfs/setup.js +++ b/lib/internal/vfs/setup.js @@ -33,7 +33,6 @@ const { normalizeMountedPath } = require('internal/vfs/file_system'); const { getLayerIdFromPath, getNormalizedVfsRoot, - isUnderMountPoint, } = require('internal/vfs/router'); const { getVirtualFd, closeVirtualFd } = require('internal/vfs/fd'); const { assertEncoding, setVfsHandlers } = require('internal/fs/utils'); @@ -326,7 +325,6 @@ function serializePackageJSON(parsed, filePath) { * @returns {{ vfs?: object, pjsonPath?: string, parsed?: object, sentinel: string }} */ function findVFSPackageJSON(vfs, startPath, normalizedStart) { - const normalizedMount = normalizeMountedPath(vfs.mountPoint); let currentDir = dirname(startPath); let currentNorm = dirname(normalizedStart); let lastDir; @@ -336,7 +334,7 @@ function findVFSPackageJSON(vfs, startPath, normalizedStart) { StringPrototypeEndsWith(currentDir, '\\node_modules')) { break; } - if (!isUnderMountPoint(currentNorm, normalizedMount)) { + if (!vfs.shouldHandleNormalized(currentNorm)) { // Walked above the mount point: nothing between the mount point // and the file-system root can exist (the namespace lives under // os.devNull, a device that cannot have children). diff --git a/test/parallel/test-vfs-router.js b/test/parallel/test-vfs-router.js index bb2656dee30e0c..292e33e7379acd 100644 --- a/test/parallel/test-vfs-router.js +++ b/test/parallel/test-vfs-router.js @@ -9,38 +9,11 @@ require('../common'); const assert = require('assert'); const path = require('path'); -const { isUnderMountPoint, getRelativePath } = - require('internal/vfs/router'); +const { getRelativePath } = require('internal/vfs/router'); const mount = path.resolve('/app'); const nested = path.join(mount, 'src', 'index.js'); -// isUnderMountPoint: equal paths are always considered "under" -assert.strictEqual(isUnderMountPoint(mount, mount), true); - -// isUnderMountPoint: nested paths -assert.strictEqual(isUnderMountPoint(nested, mount), true); - -// isUnderMountPoint: rejects sibling paths that share the prefix string -assert.strictEqual( - isUnderMountPoint(path.resolve('/app2/index.js'), mount), false, -); -assert.strictEqual(isUnderMountPoint(path.resolve('/applebrick'), mount), - false); - -// isUnderMountPoint: rejects an unrelated absolute path -assert.strictEqual(isUnderMountPoint(path.resolve('/other'), mount), false); - -// isUnderMountPoint: root mount matches any absolute path on POSIX. -// On Windows the root mount '/' resolves to a drive-letter root, so the -// special-case in router.js only applies when mountPoint === '/'. Skip the -// root-mount checks where they would not be representative on Windows. -if (process.platform !== 'win32') { - assert.strictEqual(isUnderMountPoint('/anywhere', '/'), true); - assert.strictEqual(isUnderMountPoint('/', '/'), true); - assert.strictEqual(isUnderMountPoint('/a/b/c', '/'), true); -} - // getRelativePath: equal => '/' assert.strictEqual(getRelativePath(mount, mount), '/'); @@ -48,11 +21,6 @@ assert.strictEqual(getRelativePath(mount, mount), '/'); // the platform-native input separators. assert.strictEqual(getRelativePath(nested, mount), '/src/index.js'); -// getRelativePath: root mount returns the original (already absolute) path -if (process.platform !== 'win32') { - assert.strictEqual(getRelativePath('/foo/bar', '/'), '/foo/bar'); -} - // getRelativePath: deeper nesting const mountA = path.resolve('/m/a'); const deep = path.join(mountA, 'b', 'c', 'd'); From 6fafe0733b369e311d2a92c652dc6e42319f4367 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 13 Jul 2026 11:52:28 +0200 Subject: [PATCH 22/31] vfs,esm: share legacyMainResolveExtensions arrays Signed-off-by: Matteo Collina --- lib/internal/modules/esm/resolve.js | 31 ++----------------------- lib/internal/modules/helpers.js | 35 +++++++++++++++++++++++++++++ lib/internal/vfs/setup.js | 29 +++++++++++------------- 3 files changed, 50 insertions(+), 45 deletions(-) diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index a889071e107df5..00ba363ebc985e 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -46,6 +46,8 @@ const { defaultGetFormatWithoutErrors } = require('internal/modules/esm/get_form const { getConditionsSet } = require('internal/modules/esm/utils'); const packageJsonReader = require('internal/modules/package_json_reader'); const { + legacyMainResolveExtensions, + legacyMainResolveExtensionsIndexes, loaderLegacyMainResolve, loaderStat, toRealPath, @@ -150,35 +152,6 @@ function emitLegacyIndexDeprecation(url, path, pkgPath, base, main) { } } -const legacyMainResolveExtensions = [ - '', - '.js', - '.json', - '.node', - '/index.js', - '/index.json', - '/index.node', - './index.js', - './index.json', - './index.node', -]; - -const legacyMainResolveExtensionsIndexes = { - // 0-6: when packageConfig.main is defined - kResolvedByMain: 0, - kResolvedByMainJs: 1, - kResolvedByMainJson: 2, - kResolvedByMainNode: 3, - kResolvedByMainIndexJs: 4, - kResolvedByMainIndexJson: 5, - kResolvedByMainIndexNode: 6, - // 7-9: when packageConfig.main is NOT defined, - // or when the previous case didn't found the file - kResolvedByPackageAndJs: 7, - kResolvedByPackageAndJson: 8, - kResolvedByPackageAndNode: 9, -}; - /** * Legacy CommonJS main resolution: * 1. let M = pkg_url + (json main field) diff --git a/lib/internal/modules/helpers.js b/lib/internal/modules/helpers.js index d4d81fe44a89c2..17b82b5f0ed226 100644 --- a/lib/internal/modules/helpers.js +++ b/lib/internal/modules/helpers.js @@ -701,6 +701,39 @@ function getRequireStack(parent) { return requireStack; } +// Candidate suffixes tried by legacy CommonJS `main` resolution. The +// indexes returned by the C++ binding (and the VFS override in +// internal/vfs/setup.js) map directly into this array; indexes 0..6 +// use packageConfig.main as the prefix, 7..9 use pkgPath directly. +const legacyMainResolveExtensions = [ + '', + '.js', + '.json', + '.node', + '/index.js', + '/index.json', + '/index.node', + './index.js', + './index.json', + './index.node', +]; + +const legacyMainResolveExtensionsIndexes = { + // 0-6: when packageConfig.main is defined + kResolvedByMain: 0, + kResolvedByMainJs: 1, + kResolvedByMainJson: 2, + kResolvedByMainNode: 3, + kResolvedByMainIndexJs: 4, + kResolvedByMainIndexJson: 5, + kResolvedByMainIndexNode: 6, + // 7-9: when packageConfig.main is NOT defined, + // or when the previous case didn't found the file + kResolvedByPackageAndJs: 7, + kResolvedByPackageAndJson: 8, + kResolvedByPackageAndNode: 9, +}; + module.exports = { addBuiltinLibsToObject, assertBufferSource, @@ -713,6 +746,8 @@ module.exports = { getCjsConditionsArray, getCompileCacheDir, initializeCjsConditions, + legacyMainResolveExtensions, + legacyMainResolveExtensionsIndexes, loaderGetFormatOfExtensionlessFile, loaderGetNearestParentPackageJSON, loaderGetPackageScopeConfig, diff --git a/lib/internal/vfs/setup.js b/lib/internal/vfs/setup.js index f663ec40cb806e..dd19a54b9d3fbc 100644 --- a/lib/internal/vfs/setup.js +++ b/lib/internal/vfs/setup.js @@ -876,9 +876,12 @@ function createVfsHandlers() { */ function installModuleLoaderOverrides() { const { + legacyMainResolveExtensions, + legacyMainResolveExtensionsIndexes, setLoaderFsOverrides, setLoaderPackageOverrides, } = require('internal/modules/helpers'); + const { kResolvedByMainIndexNode } = legacyMainResolveExtensionsIndexes; const internalFsBinding = internalBinding('fs'); const nativeModulesBinding = internalBinding('modules'); const { internal: internalConstants } = internalBinding('constants'); @@ -901,22 +904,16 @@ function installModuleLoaderOverrides() { legacyMainResolve(pkgPath, main, base) { if (findVFS(pkgPath) === null) return undefined; - // Extension index mapping (matches C++ legacyMainResolve): - // 0-6: try main + extension, then main + /index.ext - // 7-9: try pkgPath + ./index.ext - const mainExts = ['', '.js', '.json', '.node', - '/index.js', '/index.json', '/index.node']; - const indexExts = ['./index.js', './index.json', './index.node']; - - if (main) { - for (let i = 0; i < mainExts.length; i++) { - const candidate = join(pkgPath, main + mainExts[i]); - if (findVFSForStat(candidate)?.result === 0) return i; - } - } - for (let i = 0; i < indexExts.length; i++) { - const candidate = join(pkgPath, indexExts[i]); - if (findVFSForStat(candidate)?.result === 0) return 7 + i; + // Same candidate table as the C++ binding (indexes 0..6 build + // pkgPath/main + suffix, 7..9 build pkgPath + suffix directly). + // Sourced from internal/modules/helpers so the two loops stay + // in lockstep. + for (let i = 0; i < legacyMainResolveExtensions.length; i++) { + const byMain = i <= kResolvedByMainIndexNode; + if (byMain && !main) continue; + const prefix = byMain ? main : ''; + const candidate = join(pkgPath, prefix + legacyMainResolveExtensions[i]); + if (findVFSForStat(candidate)?.result === 0) return i; } // Match the C++ binding's message shape (see From 6e3f1b91c30afc353184f880f20e5e9d1064cff1 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 13 Jul 2026 11:56:55 +0200 Subject: [PATCH 23/31] vfs: add cleanForVfsPrefix helper for cache purges Signed-off-by: Matteo Collina --- lib/internal/modules/cjs/loader.js | 35 +++------------- lib/internal/modules/helpers.js | 45 ++++++++++++++++----- lib/internal/modules/package_json_reader.js | 27 ++++--------- 3 files changed, 48 insertions(+), 59 deletions(-) diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 301ce79439ffbb..e39e997a75483a 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -24,7 +24,6 @@ const { ArrayIsArray, ArrayPrototypeFilter, - ArrayPrototypeForEach, ArrayPrototypeIncludes, ArrayPrototypeIndexOf, ArrayPrototypeJoin, @@ -39,7 +38,6 @@ const { Error, FunctionPrototypeCall, JSONParse, - MapPrototypeForEach, ObjectDefineProperty, ObjectFreeze, ObjectGetOwnPropertyDescriptor, @@ -161,6 +159,7 @@ const assert = require('internal/assert'); const path = require('path'); const { safeGetenv } = internalBinding('credentials'); const { + cleanForVfsPrefix, getCjsConditions, getCjsConditionsArray, initializeCjsConditions, @@ -299,33 +298,11 @@ function stat(filename) { * @param {string} mountPoint Absolute mount-point path */ function purgeModuleCachesForPrefix(mountPoint) { - const prefix = mountPoint + path.sep; - const owns = (filename) => - typeof filename === 'string' && - (filename === mountPoint || StringPrototypeStartsWith(filename, prefix)); - - // Module._cache: keyed by absolute filename. - ArrayPrototypeForEach(ObjectKeys(Module._cache), (filename) => { - if (owns(filename)) { - delete Module._cache[filename]; - } - }); - - // Module._pathCache: keyed by "request\0parent" strings; the cached - // value is the resolved filename, so filter on the value. - ArrayPrototypeForEach(ObjectKeys(Module._pathCache), (key) => { - if (owns(Module._pathCache[key])) { - delete Module._pathCache[key]; - } - }); - - if (statCache !== null) { - MapPrototypeForEach(statCache, (result, filename) => { - if (owns(filename)) { - statCache.delete(filename); - } - }); - } + cleanForVfsPrefix(Module._cache, mountPoint); + // Module._pathCache keys are `request\0parent` tokens - the resolved + // filename lives in the value, so predicate on the value. + cleanForVfsPrefix(Module._pathCache, mountPoint, true); + if (statCache !== null) cleanForVfsPrefix(statCache, mountPoint); } let _stat = stat; diff --git a/lib/internal/modules/helpers.js b/lib/internal/modules/helpers.js index 17b82b5f0ed226..4c9332edc2c55f 100644 --- a/lib/internal/modules/helpers.js +++ b/lib/internal/modules/helpers.js @@ -6,6 +6,7 @@ const { MapPrototypeForEach, ObjectDefineProperty, ObjectFreeze, + ObjectKeys, ObjectPrototypeHasOwnProperty, SafeMap, SafeSet, @@ -176,21 +177,44 @@ function setLoaderPackageOverrides(overrides = kEmptyObject) { getPackageTypeOverride = overrides.getPackageType ?? null; } +/** + * Delete every entry in `cache` whose owned string is under + * `mountPoint`. `cache` is either a plain object (Module._cache, + * Module._pathCache) or a SafeMap (statCache, realpathCache, + * the package.json caches). By default the entry KEY is tested; + * pass `byValue: true` to test the entry value instead (used for + * Module._pathCache whose keys are `request\0parent` tokens and + * whose values are the resolved filenames). + * + * Mount points live in the reserved VFS namespace so a prefix scan + * on strings is exact - real-fs entries never match. + * @param {object|Map} cache + * @param {string} mountPoint Absolute mount-point path + * @param {boolean} [byValue] Test the entry value instead of the key + */ +function cleanForVfsPrefix(cache, mountPoint, byValue = false) { + const prefix = mountPoint + path.sep; + const owns = (s) => + typeof s === 'string' && + (s === mountPoint || StringPrototypeStartsWith(s, prefix)); + if (typeof cache.forEach === 'function') { + MapPrototypeForEach(cache, (v, k) => { + if (owns(byValue ? v : k)) cache.delete(k); + }); + } else { + ArrayPrototypeForEach(ObjectKeys(cache), (k) => { + if (owns(byValue ? cache[k] : k)) delete cache[k]; + }); + } +} + /** * Drop realpath-cache entries under an unmounted VFS mount point. - * Entries for unrelated paths are untouched. VFS mount points live in - * a reserved namespace that cannot collide with real paths, so a - * prefix scan on the keys is exact. Called by internal/vfs/setup.js. + * Called by internal/vfs/setup.js. * @param {string} mountPoint Absolute mount-point path */ function purgeRealpathCacheForPrefix(mountPoint) { - const prefix = mountPoint + path.sep; - MapPrototypeForEach(realpathCache, (realPath, requestPath) => { - if (requestPath === mountPoint || - StringPrototypeStartsWith(requestPath, prefix)) { - realpathCache.delete(requestPath); - } - }); + cleanForVfsPrefix(realpathCache, mountPoint); } /** @@ -737,6 +761,7 @@ const legacyMainResolveExtensionsIndexes = { module.exports = { addBuiltinLibsToObject, assertBufferSource, + cleanForVfsPrefix, purgeRealpathCacheForPrefix, constants, enableCompileCache, diff --git a/lib/internal/modules/package_json_reader.js b/lib/internal/modules/package_json_reader.js index 66ab530470a9bf..986ca4b206525c 100644 --- a/lib/internal/modules/package_json_reader.js +++ b/lib/internal/modules/package_json_reader.js @@ -3,13 +3,11 @@ const { ArrayIsArray, JSONParse, - MapPrototypeForEach, ObjectDefineProperty, RegExpPrototypeExec, SafeMap, StringPrototypeIndexOf, StringPrototypeSlice, - StringPrototypeStartsWith, } = primordials; const { fileURLToPath, @@ -29,6 +27,7 @@ const { kEmptyObject } = require('internal/util'); const path = require('path'); const { validateString } = require('internal/validators'); const { + cleanForVfsPrefix, loaderGetNearestParentPackageJSON, loaderGetPackageScopeConfig, loaderGetPackageType, @@ -362,27 +361,15 @@ function findPackageJSON(specifier, base = 'data:') { /** * Drop package.json-cache entries under an unmounted VFS mount point. - * Real-fs entries and other-VFS entries are untouched. VFS mount - * points live in a reserved namespace that cannot collide with real - * paths, so a prefix scan is exact. Called by internal/vfs/setup.js. + * Called by internal/vfs/setup.js. A module path and its owning + * package.json path always live in the same mount (the pjson search + * walks up from the module and stops at the mount root), so a single + * key test on each map is exact. * @param {string} mountPoint Absolute mount-point path */ function purgePackageJSONCacheForPrefix(mountPoint) { - const prefix = mountPoint + path.sep; - const owns = (p) => typeof p === 'string' && - (p === mountPoint || StringPrototypeStartsWith(p, prefix)); - // Keyed by module path; the value is the owning package.json path. - MapPrototypeForEach(moduleToParentPackageJSONCache, (pjsonPath, modPath) => { - if (owns(modPath) || owns(pjsonPath)) { - moduleToParentPackageJSONCache.delete(modPath); - } - }); - // Keyed by package.json path. - MapPrototypeForEach(deserializedPackageJSONCache, (config, pjsonPath) => { - if (owns(pjsonPath)) { - deserializedPackageJSONCache.delete(pjsonPath); - } - }); + cleanForVfsPrefix(moduleToParentPackageJSONCache, mountPoint); + cleanForVfsPrefix(deserializedPackageJSONCache, mountPoint); } module.exports = { From a95717ae0020a8911acdfdbc1906bfd3b5ad9cc0 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 13 Jul 2026 12:09:51 +0200 Subject: [PATCH 24/31] vfs: use indexed for loops in cleanForVfsPrefix Signed-off-by: Matteo Collina --- lib/internal/modules/helpers.js | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/internal/modules/helpers.js b/lib/internal/modules/helpers.js index 4c9332edc2c55f..ab828dba224dff 100644 --- a/lib/internal/modules/helpers.js +++ b/lib/internal/modules/helpers.js @@ -4,6 +4,7 @@ const { ArrayPrototypeForEach, ArrayPrototypePush, MapPrototypeForEach, + MapPrototypeGet, ObjectDefineProperty, ObjectFreeze, ObjectKeys, @@ -194,17 +195,23 @@ function setLoaderPackageOverrides(overrides = kEmptyObject) { */ function cleanForVfsPrefix(cache, mountPoint, byValue = false) { const prefix = mountPoint + path.sep; - const owns = (s) => - typeof s === 'string' && - (s === mountPoint || StringPrototypeStartsWith(s, prefix)); - if (typeof cache.forEach === 'function') { - MapPrototypeForEach(cache, (v, k) => { - if (owns(byValue ? v : k)) cache.delete(k); - }); - } else { - ArrayPrototypeForEach(ObjectKeys(cache), (k) => { - if (owns(byValue ? cache[k] : k)) delete cache[k]; - }); + const isMap = typeof cache.forEach === 'function'; + // Snapshot the keys up-front. For a Map this is one MapPrototypeForEach + // pass (safe against iterator pollution, matching the pattern used in + // internal/vfs/setup.js); for a plain object ObjectKeys already gives + // an owned array. The subsequent purge is a plain indexed for loop. + const keys = isMap ? [] : ObjectKeys(cache); + if (isMap) { + MapPrototypeForEach(cache, (_v, k) => ArrayPrototypePush(keys, k)); + } + for (let i = 0; i < keys.length; i++) { + const k = keys[i]; + const target = byValue ? (isMap ? MapPrototypeGet(cache, k) : cache[k]) : k; + if (typeof target === 'string' && + (target === mountPoint || StringPrototypeStartsWith(target, prefix))) { + if (isMap) cache.delete(k); + else delete cache[k]; + } } } From ffd68a2c62133e891ef03c15a7744fc0d8464486 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 13 Jul 2026 12:12:08 +0200 Subject: [PATCH 25/31] Revert "vfs: use indexed for loops in cleanForVfsPrefix" This reverts commit a95717ae0020a8911acdfdbc1906bfd3b5ad9cc0. --- lib/internal/modules/helpers.js | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/lib/internal/modules/helpers.js b/lib/internal/modules/helpers.js index ab828dba224dff..4c9332edc2c55f 100644 --- a/lib/internal/modules/helpers.js +++ b/lib/internal/modules/helpers.js @@ -4,7 +4,6 @@ const { ArrayPrototypeForEach, ArrayPrototypePush, MapPrototypeForEach, - MapPrototypeGet, ObjectDefineProperty, ObjectFreeze, ObjectKeys, @@ -195,23 +194,17 @@ function setLoaderPackageOverrides(overrides = kEmptyObject) { */ function cleanForVfsPrefix(cache, mountPoint, byValue = false) { const prefix = mountPoint + path.sep; - const isMap = typeof cache.forEach === 'function'; - // Snapshot the keys up-front. For a Map this is one MapPrototypeForEach - // pass (safe against iterator pollution, matching the pattern used in - // internal/vfs/setup.js); for a plain object ObjectKeys already gives - // an owned array. The subsequent purge is a plain indexed for loop. - const keys = isMap ? [] : ObjectKeys(cache); - if (isMap) { - MapPrototypeForEach(cache, (_v, k) => ArrayPrototypePush(keys, k)); - } - for (let i = 0; i < keys.length; i++) { - const k = keys[i]; - const target = byValue ? (isMap ? MapPrototypeGet(cache, k) : cache[k]) : k; - if (typeof target === 'string' && - (target === mountPoint || StringPrototypeStartsWith(target, prefix))) { - if (isMap) cache.delete(k); - else delete cache[k]; - } + const owns = (s) => + typeof s === 'string' && + (s === mountPoint || StringPrototypeStartsWith(s, prefix)); + if (typeof cache.forEach === 'function') { + MapPrototypeForEach(cache, (v, k) => { + if (owns(byValue ? v : k)) cache.delete(k); + }); + } else { + ArrayPrototypeForEach(ObjectKeys(cache), (k) => { + if (owns(byValue ? cache[k] : k)) delete cache[k]; + }); } } From 7b6e3ded57f7bba32399d73c15313f9194163bd6 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 13 Jul 2026 12:23:36 +0200 Subject: [PATCH 26/31] vfs: fold loader wrappers into wrapLoaderMethod factory Signed-off-by: Matteo Collina --- lib/internal/modules/helpers.js | 218 ++++++++++++-------------------- lib/internal/vfs/setup.js | 30 ++--- 2 files changed, 91 insertions(+), 157 deletions(-) diff --git a/lib/internal/modules/helpers.js b/lib/internal/modules/helpers.js index 4c9332edc2c55f..08ecb5178489ed 100644 --- a/lib/internal/modules/helpers.js +++ b/lib/internal/modules/helpers.js @@ -4,10 +4,12 @@ const { ArrayPrototypeForEach, ArrayPrototypePush, MapPrototypeForEach, + MapPrototypeGet, ObjectDefineProperty, ObjectFreeze, ObjectKeys, ObjectPrototypeHasOwnProperty, + ReflectApply, SafeMap, SafeSet, StringPrototypeCharCodeAt, @@ -65,101 +67,90 @@ let debug = require('internal/util/debuglog').debuglog('module', (fn) => { * @type {Map} */ const realpathCache = new SafeMap(); -// Toggleable loader fs overrides for VFS support. -// When null, the fast path (no VFS) is taken with zero overhead. -let statOverride = null; -let readFileOverride = null; -let realpathOverride = null; -let legacyMainResolveOverride = null; -let getFormatOfExtensionlessFileOverride = null; -/** - * Set override functions for the module loader's fs operations. - * Missing or nullish fields disable that hook (fast-path restored). - * @param {{ stat?: Function, readFile?: Function, realpath?: Function, - * legacyMainResolve?: Function, getFormatOfExtensionlessFile?: Function }} overrides - */ -function setLoaderFsOverrides(overrides = kEmptyObject) { - statOverride = overrides.stat ?? null; - readFileOverride = overrides.readFile ?? null; - realpathOverride = overrides.realpath ?? null; - legacyMainResolveOverride = overrides.legacyMainResolve ?? null; - getFormatOfExtensionlessFileOverride = overrides.getFormatOfExtensionlessFile ?? null; -} - -/** - * Wrapper for internalModuleStat that supports VFS toggle. - * @param {string} filename Absolute path to stat - * @returns {number} - */ -function loaderStat(filename) { - if (statOverride !== null) { return statOverride(filename); } - return internalFsBinding.internalModuleStat(filename); -} - -/** - * Wrapper for fs.readFileSync that supports VFS toggle. - * @param {string|URL} filename Path to read - * @param {string|object} options Read options - * @returns {string|Buffer} - */ -function loaderReadFile(filename, options) { - if (readFileOverride !== null) { - const result = readFileOverride(filename, options); - if (result !== undefined) { return result; } - } - return fs.readFileSync(filename, options); -} - -/** - * Resolves the path of a given `require` specifier, following symlinks. - * @param {string} requestPath The `require` specifier - * @returns {string} - */ -function toRealPath(requestPath) { - if (realpathOverride !== null) { - const result = realpathOverride(requestPath); - if (result !== undefined) { return result; } - } - return fs.realpathSync(requestPath, { +// Toggleable loader hooks for VFS support. Each `loader*` wrapper below +// funnels through a single factory: if an override is registered for the +// original function it is called first; anything but `undefined` short- +// circuits the wrapper, otherwise the wrapper falls through to the +// pinned native. This keeps the wrapper surface a one-liner per binding +// - adding a new binding is one `wrapLoaderMethod` call, no extra state +// variables or per-hook setter branches to keep in sync. +const nativeLoaderMethods = { + stat: (filename) => internalFsBinding.internalModuleStat(filename), + readFile: (filename, options) => fs.readFileSync(filename, options), + realpath: (requestPath) => fs.realpathSync(requestPath, { [internalFS.realpathCacheKey]: realpathCache, - }); -} + }), + legacyMainResolve: (pkgPath, main, base) => + internalFsBinding.legacyMainResolve(pkgPath, main, base), + getFormatOfExtensionlessFile: (filePath) => + internalFsBinding.getFormatOfExtensionlessFile(filePath), + readPackageJSON: (jsonPath, isESM, base, specifier) => + modulesBinding.readPackageJSON(jsonPath, isESM, base, specifier), + getNearestParentPackageJSON: (checkPath) => + modulesBinding.getNearestParentPackageJSON(checkPath), + getPackageScopeConfig: (resolved) => + modulesBinding.getPackageScopeConfig(resolved), + getPackageType: (url) => modulesBinding.getPackageType(url), +}; +const loaderOverrides = new SafeMap(); +// `hasLoaderOverrides` is a single-load fast-path gate so wrappers can +// skip the Map lookup entirely when no VFS is mounted - the common case. +let hasLoaderOverrides = false; /** - * Wrapper for internalBinding('fs').legacyMainResolve that supports VFS toggle. - * @param {string} pkgPath The package directory path - * @param {string} main The package main field - * @param {string} base The base URL string - * @returns {number} + * @param {Function} originalFn Native fallback pinned at load time. + * @returns {Function} Wrapper that consults `loaderOverrides` and falls + * through to `originalFn` when no override is set or when the + * override returns `undefined`. */ -function loaderLegacyMainResolve(pkgPath, main, base) { - if (legacyMainResolveOverride !== null) { - const result = legacyMainResolveOverride(pkgPath, main, base); - if (result !== undefined) { return result; } - } - return internalFsBinding.legacyMainResolve(pkgPath, main, base); +function wrapLoaderMethod(originalFn) { + return function loaderMethodWrapper(...args) { + if (hasLoaderOverrides) { + const override = MapPrototypeGet(loaderOverrides, originalFn); + if (override !== undefined) { + const result = ReflectApply(override, undefined, args); + if (result !== undefined) return result; + } + } + return ReflectApply(originalFn, undefined, args); + }; +} + +const loaderStat = wrapLoaderMethod(nativeLoaderMethods.stat); +const loaderReadFile = wrapLoaderMethod(nativeLoaderMethods.readFile); +const toRealPath = wrapLoaderMethod(nativeLoaderMethods.realpath); +const loaderLegacyMainResolve = wrapLoaderMethod(nativeLoaderMethods.legacyMainResolve); +const loaderGetFormatOfExtensionlessFile = + wrapLoaderMethod(nativeLoaderMethods.getFormatOfExtensionlessFile); +const loaderReadPackageJSON = wrapLoaderMethod(nativeLoaderMethods.readPackageJSON); +const loaderGetNearestParentPackageJSON = + wrapLoaderMethod(nativeLoaderMethods.getNearestParentPackageJSON); +const loaderGetPackageScopeConfig = + wrapLoaderMethod(nativeLoaderMethods.getPackageScopeConfig); +const loaderGetPackageType = wrapLoaderMethod(nativeLoaderMethods.getPackageType); + +function setLoaderOverride(key, override) { + const originalFn = nativeLoaderMethods[key]; + if (override) loaderOverrides.set(originalFn, override); + else loaderOverrides.delete(originalFn); + hasLoaderOverrides = loaderOverrides.size > 0; } /** - * Wrapper for internalBinding('fs').getFormatOfExtensionlessFile that supports VFS toggle. - * @param {string} path The file path - * @returns {number} + * Set override functions for the module loader's fs operations. + * Missing or nullish fields disable that hook (fast-path restored). + * @param {{ stat?: Function, readFile?: Function, realpath?: Function, + * legacyMainResolve?: Function, getFormatOfExtensionlessFile?: Function }} overrides */ -function loaderGetFormatOfExtensionlessFile(path) { - if (getFormatOfExtensionlessFileOverride !== null) { - const result = getFormatOfExtensionlessFileOverride(path); - if (result !== undefined) { return result; } - } - return internalFsBinding.getFormatOfExtensionlessFile(path); +function setLoaderFsOverrides(overrides = kEmptyObject) { + setLoaderOverride('stat', overrides.stat); + setLoaderOverride('readFile', overrides.readFile); + setLoaderOverride('realpath', overrides.realpath); + setLoaderOverride('legacyMainResolve', overrides.legacyMainResolve); + setLoaderOverride('getFormatOfExtensionlessFile', overrides.getFormatOfExtensionlessFile); } -// Toggleable overrides for package.json C++ methods (VFS support). -let readPackageJSONOverride = null; -let getNearestParentPackageJSONOverride = null; -let getPackageScopeConfigOverride = null; -let getPackageTypeOverride = null; - /** * Set override functions for the module loader's package.json operations. * Missing or nullish fields disable that hook (fast-path restored). @@ -171,10 +162,10 @@ let getPackageTypeOverride = null; * }} overrides */ function setLoaderPackageOverrides(overrides = kEmptyObject) { - readPackageJSONOverride = overrides.readPackageJSON ?? null; - getNearestParentPackageJSONOverride = overrides.getNearestParentPackageJSON ?? null; - getPackageScopeConfigOverride = overrides.getPackageScopeConfig ?? null; - getPackageTypeOverride = overrides.getPackageType ?? null; + setLoaderOverride('readPackageJSON', overrides.readPackageJSON); + setLoaderOverride('getNearestParentPackageJSON', overrides.getNearestParentPackageJSON); + setLoaderOverride('getPackageScopeConfig', overrides.getPackageScopeConfig); + setLoaderOverride('getPackageType', overrides.getPackageType); } /** @@ -217,57 +208,6 @@ function purgeRealpathCacheForPrefix(mountPoint) { cleanForVfsPrefix(realpathCache, mountPoint); } -/** - * Wrapper for modulesBinding.readPackageJSON that supports VFS toggle. - * @param {string} jsonPath - * @param {boolean} isESM - * @param {string} base - * @param {string} specifier - * @returns {object|undefined} - */ -function loaderReadPackageJSON(jsonPath, isESM, base, specifier) { - if (readPackageJSONOverride !== null) { - return readPackageJSONOverride(jsonPath, isESM, base, specifier); - } - return modulesBinding.readPackageJSON(jsonPath, isESM, base, specifier); -} - -/** - * Wrapper for modulesBinding.getNearestParentPackageJSON that supports VFS toggle. - * @param {string} checkPath - * @returns {object|undefined} - */ -function loaderGetNearestParentPackageJSON(checkPath) { - if (getNearestParentPackageJSONOverride !== null) { - return getNearestParentPackageJSONOverride(checkPath); - } - return modulesBinding.getNearestParentPackageJSON(checkPath); -} - -/** - * Wrapper for modulesBinding.getPackageScopeConfig that supports VFS toggle. - * @param {string} resolved - * @returns {object|string} - */ -function loaderGetPackageScopeConfig(resolved) { - if (getPackageScopeConfigOverride !== null) { - return getPackageScopeConfigOverride(resolved); - } - return modulesBinding.getPackageScopeConfig(resolved); -} - -/** - * Wrapper for modulesBinding.getPackageType that supports VFS toggle. - * @param {string} url - * @returns {string|undefined} - */ -function loaderGetPackageType(url) { - if (getPackageTypeOverride !== null) { - return getPackageTypeOverride(url); - } - return modulesBinding.getPackageType(url); -} - /** @type {Set} */ let cjsConditions; /** @type {string[]} */ diff --git a/lib/internal/vfs/setup.js b/lib/internal/vfs/setup.js index dd19a54b9d3fbc..55aa6096c0e304 100644 --- a/lib/internal/vfs/setup.js +++ b/lib/internal/vfs/setup.js @@ -882,15 +882,17 @@ function installModuleLoaderOverrides() { setLoaderPackageOverrides, } = require('internal/modules/helpers'); const { kResolvedByMainIndexNode } = legacyMainResolveExtensionsIndexes; - const internalFsBinding = internalBinding('fs'); - const nativeModulesBinding = internalBinding('modules'); const { internal: internalConstants } = internalBinding('constants'); + // Each override returns `undefined` for a path the VFS does not own; + // `wrapLoaderMethod` in internal/modules/helpers then falls through + // to the pinned native binding. Same rule for the package.json + // overrides below - no manual native re-dispatch needed here. + setLoaderFsOverrides({ stat(filename) { const result = findVFSForStat(filename); - if (result !== null) return result.result; - return internalFsBinding.internalModuleStat(filename); + return result !== null ? result.result : undefined; }, readFile(filename, options) { const pathStr = typeof filename === 'string' ? filename : @@ -953,9 +955,7 @@ function installModuleLoaderOverrides() { setLoaderPackageOverrides({ readPackageJSON(jsonPath, isESM, base, specifier) { const r = findVFS(jsonPath); - if (r === null) { - return nativeModulesBinding.readPackageJSON(jsonPath, isESM, base, specifier); - } + if (r === null) return undefined; const { vfs } = r; if (vfsStat(vfs, jsonPath) !== 0) return undefined; let content; @@ -979,9 +979,7 @@ function installModuleLoaderOverrides() { }, getNearestParentPackageJSON(checkPath) { const r = findVFS(checkPath); - if (r === null) { - return nativeModulesBinding.getNearestParentPackageJSON(checkPath); - } + if (r === null) return undefined; const found = findVFSPackageJSON(r.vfs, checkPath, r.normalized); if (found.parsed !== undefined) { return serializePackageJSON(found.parsed, found.pjsonPath); @@ -994,15 +992,13 @@ function installModuleLoaderOverrides() { try { filePath = fileURLToPath(resolved); } catch { - return nativeModulesBinding.getPackageScopeConfig(resolved); + return undefined; } } else { filePath = resolved; } const r = findVFS(filePath); - if (r === null) { - return nativeModulesBinding.getPackageScopeConfig(resolved); - } + if (r === null) return undefined; const found = findVFSPackageJSON(r.vfs, filePath, r.normalized); if (found.parsed !== undefined) { return serializePackageJSON(found.parsed, found.pjsonPath); @@ -1017,15 +1013,13 @@ function installModuleLoaderOverrides() { try { filePath = fileURLToPath(url); } catch { - return nativeModulesBinding.getPackageType(url); + return undefined; } } else { filePath = url; } const r = findVFS(filePath); - if (r === null) { - return nativeModulesBinding.getPackageType(url); - } + if (r === null) return undefined; const found = findVFSPackageJSON(r.vfs, filePath, r.normalized); if (found.parsed !== undefined) { // Route through serializePackageJSON so a malformed `type` From 2e463ca11f0c9469daa0f9273483d44c92fe41fa Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 13 Jul 2026 12:42:41 +0200 Subject: [PATCH 27/31] vfs: fix lint on wrapLoaderMethod curly braces Signed-off-by: Matteo Collina --- lib/internal/modules/cjs/loader.js | 2 +- lib/internal/modules/helpers.js | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index e39e997a75483a..4c12f882b7cbb7 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -302,7 +302,7 @@ function purgeModuleCachesForPrefix(mountPoint) { // Module._pathCache keys are `request\0parent` tokens - the resolved // filename lives in the value, so predicate on the value. cleanForVfsPrefix(Module._pathCache, mountPoint, true); - if (statCache !== null) cleanForVfsPrefix(statCache, mountPoint); + if (statCache !== null) { cleanForVfsPrefix(statCache, mountPoint); } } let _stat = stat; diff --git a/lib/internal/modules/helpers.js b/lib/internal/modules/helpers.js index 08ecb5178489ed..17f0e38bf7bbee 100644 --- a/lib/internal/modules/helpers.js +++ b/lib/internal/modules/helpers.js @@ -110,7 +110,7 @@ function wrapLoaderMethod(originalFn) { const override = MapPrototypeGet(loaderOverrides, originalFn); if (override !== undefined) { const result = ReflectApply(override, undefined, args); - if (result !== undefined) return result; + if (result !== undefined) { return result; } } } return ReflectApply(originalFn, undefined, args); @@ -132,8 +132,11 @@ const loaderGetPackageType = wrapLoaderMethod(nativeLoaderMethods.getPackageType function setLoaderOverride(key, override) { const originalFn = nativeLoaderMethods[key]; - if (override) loaderOverrides.set(originalFn, override); - else loaderOverrides.delete(originalFn); + if (override) { + loaderOverrides.set(originalFn, override); + } else { + loaderOverrides.delete(originalFn); + } hasLoaderOverrides = loaderOverrides.size > 0; } @@ -190,11 +193,11 @@ function cleanForVfsPrefix(cache, mountPoint, byValue = false) { (s === mountPoint || StringPrototypeStartsWith(s, prefix)); if (typeof cache.forEach === 'function') { MapPrototypeForEach(cache, (v, k) => { - if (owns(byValue ? v : k)) cache.delete(k); + if (owns(byValue ? v : k)) { cache.delete(k); } }); } else { ArrayPrototypeForEach(ObjectKeys(cache), (k) => { - if (owns(byValue ? cache[k] : k)) delete cache[k]; + if (owns(byValue ? cache[k] : k)) { delete cache[k]; } }); } } From 5b71a94443159511ccb2f8146906c9e86a1df5c6 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 13 Jul 2026 13:03:39 +0200 Subject: [PATCH 28/31] vfs: throw ERR_INVALID_PACKAGE_CONFIG on malformed ancestor pjson Signed-off-by: Matteo Collina --- lib/internal/vfs/setup.js | 23 +++++++++++--- .../parallel/test-vfs-invalid-package-json.js | 30 ++++++++++++++++++- test/parallel/test-vfs-module-hooks.mjs | 9 ++++-- 3 files changed, 54 insertions(+), 8 deletions(-) diff --git a/lib/internal/vfs/setup.js b/lib/internal/vfs/setup.js index 55aa6096c0e304..455cfea72098b6 100644 --- a/lib/internal/vfs/setup.js +++ b/lib/internal/vfs/setup.js @@ -343,12 +343,27 @@ function findVFSPackageJSON(vfs, startPath, normalizedStart) { const pjsonPath = join(currentDir, 'package.json'); sentinel = pjsonPath; if (vfsStat(vfs, pjsonPath) === 0) { + let content; try { - const content = vfs.readFileSync(pjsonPath, 'utf8'); - const parsed = JSONParse(content); - return { vfs, pjsonPath, parsed, sentinel: pjsonPath }; + content = vfs.readFileSync(pjsonPath, 'utf8'); } catch { - // SyntaxError or other errors, continue walking + // File vanished or is unreadable after stat said it existed. + // Keep walking - matches the "no package.json here" case in + // native GetPackageJSON when ReadFileSync fails. + content = null; + } + if (content !== null) { + let parsed; + try { + parsed = JSONParse(content); + } catch (err) { + // Match native GetPackageJSON: syntactically malformed + // package.json aborts the walk with ERR_INVALID_PACKAGE_CONFIG + // instead of silently continuing past a corrupt file. + throw new ERR_INVALID_PACKAGE_CONFIG( + pjsonPath, undefined, err.message); + } + return { vfs, pjsonPath, parsed, sentinel: pjsonPath }; } } lastDir = currentDir; diff --git a/test/parallel/test-vfs-invalid-package-json.js b/test/parallel/test-vfs-invalid-package-json.js index 0b07f6979e5092..6a02170a968124 100644 --- a/test/parallel/test-vfs-invalid-package-json.js +++ b/test/parallel/test-vfs-invalid-package-json.js @@ -1,7 +1,7 @@ // Flags: --experimental-vfs --expose-internals 'use strict'; -require('../common'); +const common = require('../common'); const assert = require('assert'); const vfs = require('node:vfs'); @@ -45,3 +45,31 @@ const vfs = require('node:vfs'); myVfs.unmount(); } + +// A malformed ancestor package.json aborts the scope walk with +// ERR_INVALID_PACKAGE_CONFIG for both CJS and ESM. +{ + const myVfs = vfs.create(); + myVfs.mkdirSync('/outer/inner', { recursive: true }); + myVfs.writeFileSync('/outer/package.json', '{ bad }'); + myVfs.writeFileSync('/outer/inner/mod.js', 'module.exports = 1;'); + const mountPoint = myVfs.mount(); + + assert.throws(() => require(`${mountPoint}/outer/inner/mod.js`), + { code: 'ERR_INVALID_PACKAGE_CONFIG' }); + + myVfs.unmount(); +} + +(async () => { + const myVfs = vfs.create(); + myVfs.mkdirSync('/outer2/inner', { recursive: true }); + myVfs.writeFileSync('/outer2/package.json', '{ bad }'); + myVfs.writeFileSync('/outer2/inner/mod.js', 'export const x = 1;'); + const mountPoint = myVfs.mount(); + + await assert.rejects(() => import(`${mountPoint}/outer2/inner/mod.js`), + { code: 'ERR_INVALID_PACKAGE_CONFIG' }); + + myVfs.unmount(); +})().then(common.mustCall()); diff --git a/test/parallel/test-vfs-module-hooks.mjs b/test/parallel/test-vfs-module-hooks.mjs index 533cda749a8592..4dffdb7e11febf 100644 --- a/test/parallel/test-vfs-module-hooks.mjs +++ b/test/parallel/test-vfs-module-hooks.mjs @@ -656,9 +656,12 @@ const vfsImport = (path) => pathToFileURL(path).href; myVfs.writeFileSync('/no-type.js', 'module.exports = { noType: true };'); const mountPoint = myVfs.mount(); - // Should treat as 'none' (commonjs) since package.json is invalid - const result = require(`${mountPoint}/no-type.js`); - assert.strictEqual(result.noType, true); + // Aborts with ERR_INVALID_PACKAGE_CONFIG - matches native + // GetPackageJSON, which throws on the first malformed package.json + // encountered on the way up rather than silently treating it as + // "none". + assert.throws(() => require(`${mountPoint}/no-type.js`), + { code: 'ERR_INVALID_PACKAGE_CONFIG' }); myVfs.unmount(); } From 30d5115e21280d027106d03c65768aa3a1a11a6b Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 13 Jul 2026 13:18:05 +0200 Subject: [PATCH 29/31] src: split GetPackageJSON and expose parsePackageJSON binding Signed-off-by: Matteo Collina --- src/node_modules.cc | 128 ++++++++++++++++++++++++++++++++------------ src/node_modules.h | 19 +++++++ 2 files changed, 112 insertions(+), 35 deletions(-) diff --git a/src/node_modules.cc b/src/node_modules.cc index 717d84a4f89aaf..c2d5327c5ac5e9 100644 --- a/src/node_modules.cc +++ b/src/node_modules.cc @@ -91,37 +91,16 @@ Local BindingData::PackageConfig::Serialize(Realm* realm) const { return Array::New(isolate, values, 6); } -const BindingData::PackageConfig* BindingData::GetPackageJSON( - Realm* realm, std::string_view path, ErrorContext* error_context) { +bool BindingData::ParsePackageJSON(Realm* realm, + PackageConfig* package_config, + ErrorContext* error_context) { auto binding_data = realm->GetBindingData(); - - auto cache_entry = binding_data->package_configs_.find(path.data()); - if (cache_entry != binding_data->package_configs_.end()) { - auto& cache_value = cache_entry->second; - if (cache_value) { - return &*cache_value; - } - - // If we have a cache entry without a value, we've already - // attempted to open and read this path and couldn't (it most - // likely doesn't exist) - return nullptr; - } - - PackageConfig package_config{}; - package_config.file_path = path; - // No need to exclude BOM since simdjson will skip it. - if (ReadFileSync(&package_config.raw_json, path.data()) < 0) { - // Add `nullopt` to the package config cache so that we don't - // need to open and attempt to read this path again - binding_data->package_configs_.insert({std::string(path), std::nullopt}); - return nullptr; - } + const std::string_view path = package_config->file_path; simdjson::ondemand::document document; simdjson::ondemand::object main_object; simdjson::error_code error = - binding_data->json_parser.iterate(simdjson::pad(package_config.raw_json)) + binding_data->json_parser.iterate(simdjson::pad(package_config->raw_json)) .get(document); const auto throw_invalid_package_config = [error_context, path, realm]() { @@ -144,7 +123,7 @@ const BindingData::PackageConfig* BindingData::GetPackageJSON( realm->isolate(), "Invalid package config %s.", path.data()); } - return nullptr; + return false; }; if (error || document.get_object().get(main_object)) { @@ -169,12 +148,12 @@ const BindingData::PackageConfig* BindingData::GetPackageJSON( if (key == "name") { // Though there is a key "name" with a corresponding value, // the value may not be a string or could be an invalid JSON string - if (value.get_string(package_config.name)) { + if (value.get_string(package_config->name)) { return throw_invalid_package_config(); } } else if (key == "main") { // Omit all non-string values - USE(value.get_string(package_config.main)); + USE(value.get_string(package_config->main)); } else if (key == "exports") { if (value.type().get(field_type)) { return throw_invalid_package_config(); @@ -185,11 +164,11 @@ const BindingData::PackageConfig* BindingData::GetPackageJSON( if (value.raw_json().get(field_value)) { return throw_invalid_package_config(); } - package_config.exports = field_value; + package_config->exports = field_value; break; } case simdjson::ondemand::json_type::string: { - if (value.get_string(package_config.exports)) { + if (value.get_string(package_config->exports)) { return throw_invalid_package_config(); } break; @@ -207,11 +186,11 @@ const BindingData::PackageConfig* BindingData::GetPackageJSON( if (value.raw_json().get(field_value)) { return throw_invalid_package_config(); } - package_config.imports = field_value; + package_config->imports = field_value; break; } case simdjson::ondemand::json_type::string: { - if (value.get_string(package_config.imports)) { + if (value.get_string(package_config->imports)) { return throw_invalid_package_config(); } break; @@ -226,7 +205,7 @@ const BindingData::PackageConfig* BindingData::GetPackageJSON( // Only update type if it is "commonjs" or "module" // The default value is "none" for backward compatibility. if (field_value == "commonjs" || field_value == "module") { - package_config.type = field_value; + package_config->type = field_value; } } else if (key == "scripts") { if (value.type().get(field_type)) { @@ -237,7 +216,7 @@ const BindingData::PackageConfig* BindingData::GetPackageJSON( if (value.raw_json().get(field_value)) { return throw_invalid_package_config(); } - package_config.scripts = field_value; + package_config->scripts = field_value; break; } default: @@ -245,6 +224,40 @@ const BindingData::PackageConfig* BindingData::GetPackageJSON( } } } + return true; +} + +const BindingData::PackageConfig* BindingData::GetPackageJSON( + Realm* realm, std::string_view path, ErrorContext* error_context) { + auto binding_data = realm->GetBindingData(); + + auto cache_entry = binding_data->package_configs_.find(path.data()); + if (cache_entry != binding_data->package_configs_.end()) { + auto& cache_value = cache_entry->second; + if (cache_value) { + return &*cache_value; + } + + // If we have a cache entry without a value, we've already + // attempted to open and read this path and couldn't (it most + // likely doesn't exist) + return nullptr; + } + + PackageConfig package_config{}; + package_config.file_path = path; + // No need to exclude BOM since simdjson will skip it. + if (ReadFileSync(&package_config.raw_json, path.data()) < 0) { + // Add `nullopt` to the package config cache so that we don't + // need to open and attempt to read this path again + binding_data->package_configs_.insert({std::string(path), std::nullopt}); + return nullptr; + } + + if (!ParsePackageJSON(realm, &package_config, error_context)) { + return nullptr; + } + // package_config could be quite large, so we should move it instead of // copying it. auto cached = binding_data->package_configs_.insert( @@ -291,6 +304,49 @@ void BindingData::ReadPackageJSON(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(package_json->Serialize(realm)); } +void BindingData::ParsePackageJSONBinding( + const FunctionCallbackInfo& args) { + // parsePackageJSON(source, jsonPath, isESM, base, specifier) + CHECK_GE(args.Length(), 2); + CHECK(args[0]->IsString() || args[0]->IsArrayBufferView()); // source + CHECK(args[1]->IsString()); // jsonPath + + Realm* realm = Realm::GetCurrent(args); + auto isolate = realm->isolate(); + + BufferValue json_path(isolate, args[1]); + bool is_esm = args.Length() >= 3 && args[2]->IsTrue(); + auto error_context = ErrorContext(); + if (is_esm) { + CHECK(args[3]->IsUndefined() || args[3]->IsString()); // base + CHECK(args[4]->IsString()); // specifier + + if (args[3]->IsString()) { + Utf8Value base_value(isolate, args[3]); + error_context.base = base_value.ToString(); + } + Utf8Value specifier(isolate, args[4]); + error_context.specifier = specifier.ToString(); + } + + PackageConfig package_config{}; + package_config.file_path = json_path.ToStringView(); + if (args[0]->IsString()) { + Utf8Value source(isolate, args[0]); + package_config.raw_json = source.ToString(); + } else { + ArrayBufferViewContents view(args[0]); + package_config.raw_json = std::string(view.data(), view.length()); + } + + if (!ParsePackageJSON( + realm, &package_config, is_esm ? &error_context : nullptr)) { + return; // ParsePackageJSON already threw + } + + args.GetReturnValue().Set(package_config.Serialize(realm)); +} + const BindingData::PackageConfig* BindingData::TraverseParent( Realm* realm, const std::filesystem::path& check_path) { std::filesystem::path current_path = check_path; @@ -612,6 +668,7 @@ void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data, Local target) { Isolate* isolate = isolate_data->isolate(); SetMethod(isolate, target, "readPackageJSON", ReadPackageJSON); + SetMethod(isolate, target, "parsePackageJSON", ParsePackageJSONBinding); SetMethod(isolate, target, "getNearestParentPackageJSONType", @@ -690,6 +747,7 @@ void BindingData::CreatePerContextProperties(Local target, void BindingData::RegisterExternalReferences( ExternalReferenceRegistry* registry) { registry->Register(ReadPackageJSON); + registry->Register(ParsePackageJSONBinding); registry->Register(GetNearestParentPackageJSONType); registry->Register(GetNearestParentPackageJSON); registry->Register(GetPackageScopeConfig); diff --git a/src/node_modules.h b/src/node_modules.h index d610306a3a3111..2dbcf983d960c3 100644 --- a/src/node_modules.h +++ b/src/node_modules.h @@ -55,6 +55,17 @@ class BindingData : public SnapshotableObject { SET_MEMORY_INFO_NAME(BindingData) static void ReadPackageJSON(const v8::FunctionCallbackInfo& args); + // Parses a package.json body supplied by the caller and returns the + // same tuple as ReadPackageJSON. Does NOT populate the persistent + // `package_configs_` cache - callers that need caching (e.g. the VFS + // loader override) manage their own JS-side cache. + // + // Signature: parsePackageJSON(source, jsonPath, isESM, base, specifier) + // `source` - string or Buffer containing the raw package.json body + // `jsonPath` - absolute path used only for error messages / cache keys + // `isESM`, `base`, `specifier` - same as readPackageJSON + static void ParsePackageJSONBinding( + const v8::FunctionCallbackInfo& args); static void GetNearestParentPackageJSON( const v8::FunctionCallbackInfo& args); static void GetNearestParentPackageJSONType( @@ -90,6 +101,14 @@ class BindingData : public SnapshotableObject { Realm* realm, std::string_view path, ErrorContext* error_context = nullptr); + // Parses an already-loaded package.json body into `package_config`. + // `package_config->raw_json` and `package_config->file_path` must be + // populated by the caller before entry. On success returns true; on a + // syntactic error throws ERR_INVALID_PACKAGE_CONFIG and returns false. + // Callers are responsible for cache insertion decisions. + static bool ParsePackageJSON(Realm* realm, + PackageConfig* package_config, + ErrorContext* error_context); static const PackageConfig* TraverseParent( Realm* realm, const std::filesystem::path& check_path); }; From 337c130a29ad794b20820569556cc2266b71ea89 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 13 Jul 2026 13:18:23 +0200 Subject: [PATCH 30/31] vfs: use native parsePackageJSON binding, drop serializePackageJSON Signed-off-by: Matteo Collina --- lib/internal/vfs/setup.js | 140 ++++++++------------------------------ 1 file changed, 28 insertions(+), 112 deletions(-) diff --git a/lib/internal/vfs/setup.js b/lib/internal/vfs/setup.js index 455cfea72098b6..9f71e5dc8fc76a 100644 --- a/lib/internal/vfs/setup.js +++ b/lib/internal/vfs/setup.js @@ -1,10 +1,7 @@ 'use strict'; const { - ArrayIsArray, ArrayPrototypeForEach, - JSONParse, - JSONStringify, MapPrototypeForEach, ObjectKeys, PromiseResolve, @@ -23,7 +20,6 @@ const { validateObject } = require('internal/validators'); const { codes: { ERR_INVALID_ARG_TYPE, - ERR_INVALID_PACKAGE_CONFIG, ERR_INVALID_STATE, ERR_MODULE_NOT_FOUND, }, @@ -38,6 +34,7 @@ const { getVirtualFd, closeVirtualFd } = require('internal/vfs/fd'); const { assertEncoding, setVfsHandlers } = require('internal/fs/utils'); const permission = require('internal/process/permission'); const { getOptionValue } = require('internal/options'); +const nativeModulesBinding = internalBinding('modules'); let debug = require('internal/util/debuglog').debuglog('vfs', (fn) => { debug = fn; }); @@ -243,86 +240,23 @@ function findVFSForRead(filename, options) { } } -/** - * Serialize a parsed package.json object into the C++ tuple format - * expected by deserializePackageJSON: [name, main, type, imports, exports, filePath]. - * - * Matches the native binding's validation in src/node_modules.cc so we - * neither over- nor under-validate compared to the real-fs path: - * - parsed itself must be a non-null object (throws otherwise) - * - "name" must be a string when present (throws otherwise) - * - "main" non-strings are silently omitted - * - "type" must be a string when present (throws otherwise); only - * "module" / "commonjs" are kept, others default to "none" - * - "imports" / "exports" accept string / object / array; other - * types are silently ignored - * @param {object} parsed The parsed package.json content - * @param {string} filePath The path to the package.json file - * @returns {Array} Serialized package config tuple - */ -function serializePackageJSON(parsed, filePath) { - if (parsed === null || typeof parsed !== 'object' || ArrayIsArray(parsed)) { - throw new ERR_INVALID_PACKAGE_CONFIG(filePath, undefined, ''); - } - - let name; - if (parsed.name !== undefined && parsed.name !== null) { - if (typeof parsed.name !== 'string') { - throw new ERR_INVALID_PACKAGE_CONFIG( - filePath, undefined, '"name" must be a string'); - } - name = parsed.name; - } - - let main; - if (typeof parsed.main === 'string') { - main = parsed.main; - } - - let type = 'none'; - if (parsed.type !== undefined && parsed.type !== null) { - if (typeof parsed.type !== 'string') { - throw new ERR_INVALID_PACKAGE_CONFIG( - filePath, undefined, '"type" must be a string'); - } - if (parsed.type === 'module' || parsed.type === 'commonjs') { - type = parsed.type; - } - } - - let imports; - if (typeof parsed.imports === 'string') { - imports = parsed.imports; - } else if (typeof parsed.imports === 'object' && parsed.imports !== null) { - imports = JSONStringify(parsed.imports); - } - - let exports; - if (typeof parsed.exports === 'string') { - exports = parsed.exports; - } else if (typeof parsed.exports === 'object' && parsed.exports !== null) { - exports = JSONStringify(parsed.exports); - } - - return [name, main, type, imports, exports, filePath]; -} - /** * Walk up directories inside `vfs`'s mount looking for package.json. - * Always returns an object. When a package.json is found `.parsed` is - * populated; otherwise `.sentinel` is the last candidate path checked - * (highest reached before walking past the mount or hitting - * node_modules) - used as the "not found" marker matching the C++ - * binding's contract for getPackageScopeConfig. + * Always returns an object. When a package.json is found `.tuple` is + * populated with the C++ binding's serialized shape + * (`[name, main, type, imports, exports, filePath]`); otherwise + * `.sentinel` is the last candidate path checked - matches the "not + * found" marker the C++ binding returns for getPackageScopeConfig. + * Syntactically malformed package.json aborts the walk with + * ERR_INVALID_PACKAGE_CONFIG (thrown by the native parser), matching + * native GetPackageJSON. * @param {object} vfs The VFS that owns startPath - * @param {string} startPath Absolute path to start from - the result - * returned to the caller (e.g. sentinel) is built from this, so that - * error messages and cache keys keep the caller's original form. + * @param {string} startPath Absolute path to start from * @param {string} normalizedStart Same as startPath but passed through * `normalizeMountedPath`. Used to walk the directory chain so the * containment check is a plain string prefix rather than a fresh * `toNamespacedPath(resolve(...))` on every iteration. - * @returns {{ vfs?: object, pjsonPath?: string, parsed?: object, sentinel: string }} + * @returns {{ vfs?: object, pjsonPath?: string, tuple?: Array, sentinel: string }} */ function findVFSPackageJSON(vfs, startPath, normalizedStart) { let currentDir = dirname(startPath); @@ -353,17 +287,12 @@ function findVFSPackageJSON(vfs, startPath, normalizedStart) { content = null; } if (content !== null) { - let parsed; - try { - parsed = JSONParse(content); - } catch (err) { - // Match native GetPackageJSON: syntactically malformed - // package.json aborts the walk with ERR_INVALID_PACKAGE_CONFIG - // instead of silently continuing past a corrupt file. - throw new ERR_INVALID_PACKAGE_CONFIG( - pjsonPath, undefined, err.message); - } - return { vfs, pjsonPath, parsed, sentinel: pjsonPath }; + // parsePackageJSON validates types and throws + // ERR_INVALID_PACKAGE_CONFIG on any error; propagates to + // abort the walk exactly like native GetPackageJSON. + const tuple = nativeModulesBinding.parsePackageJSON( + content, pjsonPath); + return { vfs, pjsonPath, tuple, sentinel: pjsonPath }; } } lastDir = currentDir; @@ -980,26 +909,17 @@ function installModuleLoaderOverrides() { // Treat read errors as "no package.json" - same as native. return undefined; } - let parsed; - try { - parsed = JSONParse(content); - } catch (err) { - // Both CJS and ESM raise ERR_INVALID_PACKAGE_CONFIG on - // malformed JSON (see nodejs/node#48606). - throw new ERR_INVALID_PACKAGE_CONFIG(jsonPath, base, err.message); - } - // serializePackageJSON may throw ERR_INVALID_PACKAGE_CONFIG for - // wrong-type fields - intentionally not caught. - return serializePackageJSON(parsed, jsonPath); + // Native parsePackageJSON throws ERR_INVALID_PACKAGE_CONFIG on + // syntactic errors and on wrong-type fields (name/type). Both + // CJS and ESM raise the error since nodejs/node#48606. + return nativeModulesBinding.parsePackageJSON( + content, jsonPath, isESM, base, specifier); }, getNearestParentPackageJSON(checkPath) { const r = findVFS(checkPath); if (r === null) return undefined; const found = findVFSPackageJSON(r.vfs, checkPath, r.normalized); - if (found.parsed !== undefined) { - return serializePackageJSON(found.parsed, found.pjsonPath); - } - return undefined; + return found.tuple; }, getPackageScopeConfig(resolved) { let filePath; @@ -1015,9 +935,7 @@ function installModuleLoaderOverrides() { const r = findVFS(filePath); if (r === null) return undefined; const found = findVFSPackageJSON(r.vfs, filePath, r.normalized); - if (found.parsed !== undefined) { - return serializePackageJSON(found.parsed, found.pjsonPath); - } + if (found.tuple !== undefined) return found.tuple; // No package.json found anywhere up the tree - return the // topmost path that was checked. Matches the C++ binding contract. return found.sentinel; @@ -1036,12 +954,10 @@ function installModuleLoaderOverrides() { const r = findVFS(filePath); if (r === null) return undefined; const found = findVFSPackageJSON(r.vfs, filePath, r.normalized); - if (found.parsed !== undefined) { - // Route through serializePackageJSON so a malformed `type` - // (non-string) throws ERR_INVALID_PACKAGE_CONFIG, matching - // the native binding and the other two package.json - // overrides. The serialized tuple is [name, main, type, ...]. - const type = serializePackageJSON(found.parsed, found.pjsonPath)[2]; + if (found.tuple !== undefined) { + // Position 2 of the tuple is `type` per the C++ binding + // shape (name, main, type, imports, exports, filePath). + const type = found.tuple[2]; if (type === 'module' || type === 'commonjs') return type; } return undefined; From 0d5fb6135256ad68f101d1555bb8d1634324141a Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 13 Jul 2026 13:18:38 +0200 Subject: [PATCH 31/31] vfs: read pjson as Buffer, skip UTF-8 decode Signed-off-by: Matteo Collina --- lib/internal/vfs/setup.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/internal/vfs/setup.js b/lib/internal/vfs/setup.js index 9f71e5dc8fc76a..f538fe6a160f7f 100644 --- a/lib/internal/vfs/setup.js +++ b/lib/internal/vfs/setup.js @@ -279,7 +279,7 @@ function findVFSPackageJSON(vfs, startPath, normalizedStart) { if (vfsStat(vfs, pjsonPath) === 0) { let content; try { - content = vfs.readFileSync(pjsonPath, 'utf8'); + content = vfs.readFileSync(pjsonPath); } catch { // File vanished or is unreadable after stat said it existed. // Keep walking - matches the "no package.json here" case in @@ -904,7 +904,7 @@ function installModuleLoaderOverrides() { if (vfsStat(vfs, jsonPath) !== 0) return undefined; let content; try { - content = vfs.readFileSync(jsonPath, 'utf8'); + content = vfs.readFileSync(jsonPath); } catch { // Treat read errors as "no package.json" - same as native. return undefined;