From 66713f6a6c60b88cd86e4175f09a5501019184a5 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 16 Jun 2026 10:39:16 +0200 Subject: [PATCH 1/9] 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());