Skip to content

vfs: integrate with CJS and ESM module loaders#63653

Open
mcollina wants to merge 33 commits into
nodejs:mainfrom
mcollina:vfs-module-loader-integration
Open

vfs: integrate with CJS and ESM module loaders#63653
mcollina wants to merge 33 commits into
nodejs:mainfrom
mcollina:vfs-module-loader-integration

Conversation

@mcollina

@mcollina mcollina commented May 30, 2026

Copy link
Copy Markdown
Member

Makes require() and import resolve files served by node:vfs. Before this PR, mounted VFS files were only visible through fs.*; the loaders went straight to the real filesystem.

Design

vfs.mount() takes no arguments and returns the reserved absolute mount point of the instance: ${os.devNull}/vfs/<layerId> (for example /dev/null/vfs/0). os.devNull is a character device on POSIX and a device-namespace path on Windows; neither can have child filesystem entries, so no real path can ever exist under this root.

Everything about ownership is decidable from the path alone:

  • Dispatch: one path normalization, one prefix comparison against the namespace root, one map lookup on the layer id. O(1) in the number of mounted layers — the benchmark (benchmark/vfs/bench-fs-dispatch.js) reports flat per-call latency across 1..10 mounted layers.
  • Real-fs paths short-circuit with a single prefix check.
  • Unmount purges loader caches by prefix-scanning the mount point — no per-VFS ownership tracking is needed, and no other-VFS or real-fs entries are ever touched.
  • Symlink correctness: realpathSync of a VFS entry always resolves to another path under the same mount point, so cache entries hidden behind symlinks are captured by the prefix scan.
  • Dynamic-import identity: resolved URLs are plain file: URLs under the mount point; import(import.meta.resolve(x)) re-hits the same module job.

Two instances mounting simultaneously never collide (each gets its own layer-<id> segment), so there is no overlap validation and no ordering hazard between mounts.

Loader integration

Toggleable wrappers in the loaders. Null fast-path when no VFS is mounted; otherwise the VFS answers stat / readFile / realpath / legacyMainResolve / getFormatOfExtensionlessFile and the four package.json C++-binding calls.

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 — no synthetic decorations.

Review guide

Suggested reading order:

# File Role
1 lib/internal/vfs/router.js Reserved-namespace helpers: getVfsRoot, getLayerRoot, getLayerIdFromPath.
2 lib/internal/vfs/file_system.js mount() returns the layer's reserved mount point.
3 lib/internal/vfs/setup.js Heart of the PR. findVFS (O(1) lookup), fs handler, loader overrides with parity to src/node_modules.cc / src/node_file.cc, prefix-scan cache purge.
4 lib/internal/modules/helpers.js Hook surface: loader* wrappers, setLoaderFsOverrides / setLoaderPackageOverrides, purgeRealpathCacheForPrefix.
5 lib/internal/modules/cjs/loader.js stat() + TS read routed through wrappers; purgeModuleCachesForPrefix for unmount.
6 lib/internal/modules/esm/resolve.js legacyMainResolve + internalModuleStat + toRealPath routed. No URL decoration.
7 lib/internal/modules/esm/load.js getSourceSync reads via the wrapper.
8 lib/internal/modules/esm/get_format.js Extensionless format detection routed.
9 lib/internal/modules/package_json_reader.js 4 C++ binding calls routed; purgePackageJSONCacheForPrefix.
10 lib/fs.js statSync / lstatSync honour throwIfNoEntry:false on ENOENT from the VFS handler.
11 doc/api/vfs.md Mount semantics + Module loader integration section.

Tests (all gated by --experimental-vfs): test-vfs-mount, test-vfs-mount-errors, test-vfs-multi-mount, test-vfs-require, test-vfs-import, test-vfs-module-hooks, test-vfs-module-hooks-cleanup, test-vfs-package-json, test-vfs-package-json-cache, test-vfs-invalid-package-json, test-vfs-scoped-cache-purge, test-vfs-layer-id, test-vfs-layer-tag-prefix.

Refs

The reserved-namespace design follows the "no interference with valid paths in the file system" requirement from the SEA VFS requirements doc.

Out of scope

SEA + VFS, overlay/stacking of multiple VFS layers under one prefix, migrating the C++ package_configs_ cache, broader permission-model integration.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/loaders

@nodejs-github-bot nodejs-github-bot added lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels May 30, 2026
@codecov

codecov Bot commented May 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.03079% with 67 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.22%. Comparing base (0e2126d) to head (4c417d0).
⚠️ Report is 155 commits behind head on main.

Files with missing lines Patch % Lines
lib/internal/vfs/setup.js 86.07% 50 Missing ⚠️
src/node_modules.cc 77.96% 4 Missing and 9 partials ⚠️
lib/internal/vfs/file_system.js 96.07% 2 Missing ⚠️
lib/internal/vfs/router.js 95.45% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #63653      +/-   ##
==========================================
- Coverage   92.04%   90.22%   -1.82%     
==========================================
  Files         381      739     +358     
  Lines      169208   241834   +72626     
  Branches    25926    45681   +19755     
==========================================
+ Hits       155746   218204   +62458     
- Misses      13174    15150    +1976     
- Partials      288     8480    +8192     
Files with missing lines Coverage Δ
lib/fs.js 98.36% <100.00%> (+4.13%) ⬆️
lib/internal/modules/cjs/loader.js 98.15% <100.00%> (+18.59%) ⬆️
lib/internal/modules/esm/get_format.js 95.20% <100.00%> (+20.89%) ⬆️
lib/internal/modules/esm/load.js 91.40% <100.00%> (+8.14%) ⬆️
lib/internal/modules/esm/resolve.js 99.03% <100.00%> (+12.10%) ⬆️
lib/internal/modules/helpers.js 98.75% <100.00%> (+7.96%) ⬆️
lib/internal/modules/package_json_reader.js 99.47% <100.00%> (+11.86%) ⬆️
src/node_modules.h 100.00% <ø> (ø)
lib/internal/vfs/file_system.js 99.55% <96.07%> (+0.34%) ⬆️
lib/internal/vfs/router.js 97.01% <95.45%> (+12.23%) ⬆️
... and 2 more

... and 515 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mcollina

Copy link
Copy Markdown
Member Author

@joyeecheung take a look, should be easier to review.

Comment thread lib/internal/modules/esm/load.js
Comment thread lib/internal/modules/esm/resolve.js
mcollina added a commit to mcollina/node that referenced this pull request Jun 1, 2026
Restore the "DO NOT depend on the patchability" warnings in esm/load.js
and esm/resolve.js that were dropped along with the fs imports. The
warning still applies; it now also points at node:vfs as one of the
formal hook mechanisms callers should reach for instead.

Addresses review feedback from @jsumners-nr in
nodejs#63653
@mcollina
mcollina force-pushed the vfs-module-loader-integration branch from 6321e08 to 51b033a Compare June 1, 2026 15:54
@mcollina mcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Jun 3, 2026
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Jun 3, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@joyeecheung joyeecheung left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A design question recently occurred to me: have we explored the versioning of the mounting?

Comment thread lib/internal/vfs/setup.js Outdated
@mcollina

mcollina commented Jun 3, 2026

Copy link
Copy Markdown
Member Author

A design question recently occurred to me: have we explored the versioning of the mounting?

what do you mean? You mean multiple vfs layers on top of each other?

@joyeecheung

joyeecheung commented Jun 3, 2026

Copy link
Copy Markdown
Member

what do you mean? You mean multiple vfs layers on top of each other?

For the stacks to have some kind of version number/ID to identify the current status?

BTW I just noticed that there's no mention of unmount() and mount() in the VFS docs..

@mcollina

mcollina commented Jun 3, 2026

Copy link
Copy Markdown
Member Author

BTW I just noticed that there's no mention of unmount() and mount() in the VFS docs..

I did purge them when doing the splitting; I forgot to bring them back. I'll add them to this PR.

@mcollina

mcollina commented Jun 3, 2026

Copy link
Copy Markdown
Member Author

For the stacks to have some kind of version number/ID to identify the current status?

No but we totally should.

mcollina added a commit to mcollina/node that referenced this pull request Jun 4, 2026
Each VirtualFileSystem now exposes a per-process monotonically
increasing `layerId`, assigned at construction. The id is stable
across mount/unmount cycles for the lifetime of the instance and
surfaces in:

- debug() output for register / deregister so the layer stack is
  visible when NODE_DEBUG=vfs is enabled;
- the overlap ERR_INVALID_STATE message, which now names the layer
  ids of the conflicting mounts.

The id is the building block for tagging cache entries with the
owning VFS, which a follow-up will use to replace the global
loader-cache flush in deregisterVFS with a scoped purge.

Refs: nodejs#63653
Signed-off-by: Matteo Collina <hello@matteocollina.com>
@mcollina
mcollina force-pushed the vfs-module-loader-integration branch from 51b033a to 294a19c Compare June 4, 2026 07:23
mcollina added a commit to mcollina/node that referenced this pull request Jun 4, 2026
Replace the global loader-cache flush in deregisterVFS with a
scope-purge that only drops entries owned by the unmounting VFS.
Per-layer ownership is determined two ways:

- For CJS-style filename-keyed caches (Module._cache,
  Module._pathCache, the CJS stat cache, the helpers.js realpath
  cache, and the package.json caches) entries are filtered with
  `vfs.shouldHandle(filename)`. __filename stays a clean absolute
  path so user code that does `path.dirname(__filename)` or similar
  is unaffected.

- For the ESM cascaded loader's loadCache, entries are tagged at
  resolve time: when finalizeResolution() detects the resolved
  path is VFS-owned (via the new loaderGetLayerForPath hook), it
  appends `?vfs-layer=<id>` to the URL. The tag surfaces in
  `import.meta.url`, matching the cache-busting pattern used by
  HMR tooling. On deregister, entries whose URL carries the tag
  for the unmounting layer are deleted.

Multi-mount setups no longer pay the cross-VFS cache-warmup
penalty when a single VFS unmounts, and ESM modules loaded from a
VFS become reachable for purge instead of leaking forever in the
cascaded loader.

New helpers exposed for VFS:
- cjs/loader.js: clearStatCacheForVFS
- helpers.js: purgeRealpathCacheForVFS, loaderGetLayerForPath
- package_json_reader.js: purgePackageJSONCacheForVFS

Adds test-vfs-scoped-cache-purge covering both the multi-mount
isolation and the import.meta.url tag visibility.

Refs: nodejs#63653
Signed-off-by: Matteo Collina <hello@matteocollina.com>
@mcollina

mcollina commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

@joyeecheung PTAL

@mcollina
mcollina force-pushed the vfs-module-loader-integration branch from 75bb5c7 to 99a5a5c Compare June 15, 2026 09:00
@mcollina mcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Jul 8, 2026
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Jul 8, 2026
@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@trivikr
trivikr requested a review from joyeecheung July 9, 2026 16:55

@joyeecheung joyeecheung left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High level direction looks much better and more aligned with https://github.com/nodejs/single-executable/blob/main/docs/virtual-file-system-requirements.md#no-interference-with-valid-paths-in-the-file-system now, thanks, though I think the implementation still leaves quite a bit hard-codes and repeitions that should be cleaned up before landing to avoid taxing future changes to module loading with extra maintenance complexity & divergence..

Comment thread lib/internal/modules/helpers.js Outdated
* @returns {object|undefined}
*/
function loaderReadPackageJSON(jsonPath, isESM, base, specifier) {
if (readPackageJSONOverride !== null) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Helpers like this look like a lot of maintainance burden - every time someone adds a new binding, they have to update all these, and if they end up updating some internal parameters as part of a semver major change, it can cause another wave of churns for backports. Can you just use a helper to forward them all? Something like

function wrapLoaderMethod(originalFn) {
  return function(...args) {
    const override = overrideMap.get(originalFn);
    if (override) return ReflectApply(override, this, args);
    return ReflectApply(originalFn, this, args);
  }
}

(if a benchmark shows it actually matters for perf we could even switch on originalFn.length and build arity-based dispatches like processTicksAndRejections but I suspect the overhead is negligible for this path in the bigger picture). This would also shrink the diff here quite a bit.

Comment thread lib/internal/vfs/file_system.js Outdated
@@ -174,11 +219,26 @@ class VirtualFileSystem {
* @returns {boolean}
*/
shouldHandle(inputPath) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is dead code now?

Comment thread lib/internal/vfs/setup.js Outdated
const parsed = JSONParse(content);
return { vfs, pjsonPath, parsed, sentinel: pjsonPath };
} catch {
// SyntaxError or other errors, continue walking

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This diverges from GetPackageJSON which would throw ERR_INVALID_PACKAGE_CONFIG on the first syntactically malformed package.json instead of keep walking, I think we should at least align with the actual behavior here or otherwise this can introduce very subtle bugs for users that create a tree into VFS and one of the package.json end up corrupted somehow.

What would be even better is to simply split the C++ GetPackageJSON into read + parse, and let the VFS side reuse the parsing logic, that also eliminates the duplication of serializePackageJSON etc and would make use of partial parsing with simdjson (we could technically just read the pacakge.json as buffer and avoid the UTF8 encoding cost too) instead of a wasteful full parse with JSON.parse.

Comment thread lib/internal/vfs/setup.js Outdated
// mount. Treat that as JS (the caller will surface the real
// error when it later tries to load source). Propagate every
// other code (EACCES, ELOOP, etc).
if (e?.code === 'ENOENT') return 0; // EXTENSIONLESS_FORMAT_JAVASCRIPT

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original version normalizes all errors for EXTENSIONLESS_FORMAT_JAVASCRIPT, not just ENOENT.

Comment thread lib/internal/vfs/setup.js Outdated
Comment thread lib/internal/vfs/setup.js Outdated
if (content && content.length >= 4 &&
content[0] === 0x00 && content[1] === 0x61 &&
content[2] === 0x73 && content[3] === 0x6d) {
return 1; // EXTENSIONLESS_FORMAT_WASM

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't hard-code and should return internalBinding('constants').EXTENSIONLESS_FORMAT_WASM etc.

Comment thread lib/internal/vfs/setup.js Outdated
Comment thread lib/internal/vfs/setup.js Outdated
// 7-9: try pkgPath + ./index.ext
const mainExts = ['', '.js', '.json', '.node',
'/index.js', '/index.json', '/index.node'];
const indexExts = ['./index.js', './index.json', './index.node'];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to duplicate all these? It can go out of sync easily. legacyMainResolveExtensions is already (inappropriately) located in esm/resolve.js, I think we can simply share the arrays and do something like

legacyMainResolve(pkgPath, main, base) {
  if (findVFS(pkgPath) === null) return undefined;

  for (let i = 0; i < legacyMainResolveExtensions.length; i++) {
    const shouldResolveByMain = i <= kMaxResolvedByMainIndex; // kResolvedByMainIndexNode
    if (shouldResolveByMain && !main) continue;
    const prefix = shouldResolveByMain ? main : '';
    const candidate = join(pkgPath, prefix + legacyMainResolveExtensions[i]);
    if (findVFSForStat(candidate)?.result === 0) return i;
  }

  const initial = main ? join(pkgPath, main) : join(pkgPath, 'index.js');
  throw new ERR_MODULE_NOT_FOUND(initial, base, undefined);
}

Comment thread lib/internal/vfs/setup.js Outdated
StringPrototypeEndsWith(currentDir, '\\node_modules')) {
break;
}
const pjsonPath = join(currentDir, 'package.json');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since dirname/join preserves the prefix if it's absolute, I think can just do something like if (!vfs.shouldHandleNormalized(currentDir)) break; to save on the repeated normalization in stat/read etc.

Comment thread lib/internal/vfs/setup.js Outdated
} else {
filePath = resolved;
}
const vfs = findVFS(filePath);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: findVFS discards the normalized filePath which is a bit wasteful, if it returns that back we can reuse that in e.g. findVFSPackageJSON.

mcollina and others added 18 commits July 13, 2026 02:15
Co-authored-by: Joyee Cheung <joyeec9h3@gmail.com>
Co-authored-by: Joyee Cheung <joyeec9h3@gmail.com>
`VirtualFileSystem#shouldHandle` had no remaining callers: dispatch
goes through `shouldHandleNormalized` and every code path outside
dispatch was rewritten to `findVFS` after the reserved-namespace
refactor.

Two router.js exports were also unreferenced: `getVfsRoot` (used only
internally by `getNormalizedVfsRoot` and `getLayerRoot`) and the
re-export of `path.isAbsolute` as `isAbsolutePath`. The corresponding
test-vfs-router assertions and the `isAbsolute` local binding on the
`require('path')` destructure go with them.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
@mcollina
mcollina force-pushed the vfs-module-loader-integration branch from d1d72c3 to 0d5fb61 Compare July 13, 2026 13:59
@mcollina

Copy link
Copy Markdown
Member Author

@joyeecheung done

@trivikr
trivikr requested a review from joyeecheung July 15, 2026 17:27

@joyeecheung joyeecheung left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remove the (AI generated?) comments that just restate what happens in code with prose or stating how other code calls it currently (which can get out of sync very quickly and is anti-encapsulation?)

Also I think the documentation still doesn't clarify a core question: how is the module lookup algorithm altered once an VFS is mounted. It needs to specify that precisely in the module loading spec in modules.md instead of just leaving some hand wavy descriptions like "the module loader will consult it, it will work" (but how and where in the algorithm? and when will they NOT be consulted?)

Comment thread lib/internal/modules/esm/load.js Outdated
// Prefer module.registerHooks() or other more formal fs hooks released in the future.
source = fs.readFileSync(url);
// Prefer module.registerHooks(), node:vfs, or other more formal fs hooks
// released in the future. loaderReadFile is the toggleable hook used by

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original comment is only for those who look for fs.readFileSync to monkey patch, loaderReadFile is obviously not monkey patchable, the comment should be moved to where fs.readFileSync is actually called.

Comment thread lib/internal/modules/helpers.js Outdated
Comment on lines +71 to +77
// Toggleable loader hooks for VFS support. Each `loader*` wrapper below
// funnels through a single factory: if an override is registered for the
// original function it is called first; anything but `undefined` short-
// circuits the wrapper, otherwise the wrapper falls through to the
// pinned native. This keeps the wrapper surface a one-liner per binding
// - adding a new binding is one `wrapLoaderMethod` call, no extra state
// variables or per-hook setter branches to keep in sync.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Toggleable loader hooks for VFS support. Each `loader*` wrapper below
// funnels through a single factory: if an override is registered for the
// original function it is called first; anything but `undefined` short-
// circuits the wrapper, otherwise the wrapper falls through to the
// pinned native. This keeps the wrapper surface a one-liner per binding
// - adding a new binding is one `wrapLoaderMethod` call, no extra state
// variables or per-hook setter branches to keep in sync.
// Toggleable loader hooks for VFS support.

This is just describing what obviously happens below with 7 lines of prose?

Comment thread lib/internal/modules/helpers.js Outdated
Comment on lines +673 to +674
// internal/vfs/setup.js) map directly into this array; indexes 0..6
// use packageConfig.main as the prefix, 7..9 use pkgPath directly.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// internal/vfs/setup.js) map directly into this array; indexes 0..6
// use packageConfig.main as the prefix, 7..9 use pkgPath directly.
// internal/vfs/setup.js) map directly into this array.

This is repeating what comments below says.


// Warm-up - get the JIT past the first-call icache + IC misses so we
// measure steady-state dispatch cost, not first-call resolution.
for (let i = 0; i < 1000; i++) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is actually useful, do real world app really go through 1000+ calls for all these to hit the optimizing compiler? If not this is over fitting. It's much more realistic to at least e.g. benchmark the module loading performance of a big graph after mounting the VFS.

Comment thread doc/api/vfs.md
Comment on lines +128 to +134
Mount points always live inside a reserved namespace,
`${os.devNull}/vfs/<layerId>`. 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Mount points always live inside a reserved namespace,
`${os.devNull}/vfs/<layerId>`. 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.
Mount points always live inside a reserved namespace that cannot have child file system entries,
so virtual paths never conflate with (or shadow) real paths. The virtual path scheme is subject to
change and users should not manually construct them based on assumptions. Instead, obtain
them from what `vfs.mount()` returns or `vfs.mountPoint`.

I don't think we should make the path scheme a contract.

Comment thread doc/api/vfs.md Outdated
the last [`vfs.mount()`][] call), or `null` when the VFS is not
mounted.

### `vfs.layerId`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it really necessary to expose this?

Comment thread doc/api/vfs.md
Comment on lines +326 to +332
participate in module resolution and loading. Both
`require()` / `require.resolve()` (CommonJS) and `import` /
`import.meta.resolve()` (ECMAScript modules) consult the VFS through
the same toggleable hooks that `node:fs` uses, so files served from
the VFS are first-class modules: `package.json` is honoured,
extensionless files are sniffed for Wasm vs. JavaScript, conditional
`exports` / `imports` work, and so on.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
participate in module resolution and loading. Both
`require()` / `require.resolve()` (CommonJS) and `import` /
`import.meta.resolve()` (ECMAScript modules) consult the VFS through
the same toggleable hooks that `node:fs` uses, so files served from
the VFS are first-class modules: `package.json` is honoured,
extensionless files are sniffed for Wasm vs. JavaScript, conditional
`exports` / `imports` work, and so on.
participate in module resolution and loading. Both
`require()` / `require.resolve()` (CommonJS) and `import` /
`import.meta.resolve()` (ECMAScript modules) consult the VFS through
the same toggleable hooks that `node:fs` uses, so files served from
the VFS are first-class modules: `package.json` is honoured,
extensionless files are sniffed for Wasm vs. JavaScript, conditional
`exports` / `imports` work, and so on.

This paragraph is listing example without actually clarifying anything - exactly how do they appear in the module lookup algorithm, and with what precedence?

mcollina added 2 commits July 18, 2026 15:10
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Signed-off-by: Matteo Collina <hello@matteocollina.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. vfs Issues and PRs related to the virtual filesystem subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants