Skip to content

Commit 2ab35a1

Browse files
committed
vfs: replace fs patching with toggleable toRealPath for module loading
ESM resolve.js captures `realpathSync` via destructuring at import time, so patching `fs.realpathSync` later has no effect on ESM resolution. Replace the direct `realpathSync` call in `finalizeResolution()` with the shared `toRealPath()` from helpers, which dispatches to a VFS-aware override at runtime. Split `installHooks()` into `installModuleHooks()` (Module._stat, toRealPath override, ESM hooks) and `installFsPatches()` (fs.* patches for user code transparency) for clearer separation of concerns.
1 parent fbe9f0f commit 2ab35a1

File tree

3 files changed

+80
-29
lines changed

3 files changed

+80
-29
lines changed

lib/internal/modules/esm/resolve.js

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ const {
99
ObjectPrototypeHasOwnProperty,
1010
RegExpPrototypeExec,
1111
RegExpPrototypeSymbolReplace,
12-
SafeMap,
1312
SafeSet,
1413
String,
1514
StringPrototypeEndsWith,
@@ -23,9 +22,8 @@ const {
2322
encodeURIComponent,
2423
} = primordials;
2524
const assert = require('internal/assert');
26-
const internalFS = require('internal/fs/utils');
2725
const { BuiltinModule } = require('internal/bootstrap/realm');
28-
const { realpathSync } = require('fs');
26+
const { toRealPath } = require('internal/modules/helpers');
2927
const { getOptionValue } = require('internal/options');
3028
// Do not eagerly grab .manifest, it may be in TDZ
3129
const { sep, posix: { relative: relativePosixPath }, resolve } = require('path');
@@ -153,8 +151,6 @@ function emitLegacyIndexDeprecation(url, path, pkgPath, base, main) {
153151
}
154152
}
155153

156-
const realpathCache = new SafeMap();
157-
158154
const legacyMainResolveExtensions = [
159155
'',
160156
'.js',
@@ -277,9 +273,7 @@ function finalizeResolution(resolved, base, preserveSymlinks) {
277273
}
278274

279275
if (!preserveSymlinks) {
280-
const real = realpathSync(path, {
281-
[internalFS.realpathCacheKey]: realpathCache,
282-
});
276+
const real = toRealPath(path);
283277
const { search, hash } = resolved;
284278
resolved =
285279
pathToFileURL(real + (StringPrototypeEndsWith(path, sep) ? '/' : ''));

lib/internal/modules/helpers.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,43 @@ let debug = require('internal/util/debuglog').debuglog('module', (fn) => {
5555
* @type {Map<string, string>}
5656
*/
5757
const realpathCache = new SafeMap();
58+
59+
let customToRealPath = null;
60+
5861
/**
59-
* Resolves the path of a given `require` specifier, following symlinks.
62+
* Default implementation: resolves the path following symlinks.
6063
* @param {string} requestPath The `require` specifier
6164
* @returns {string}
6265
*/
63-
function toRealPath(requestPath) {
66+
function defaultToRealPath(requestPath) {
6467
return fs.realpathSync(requestPath, {
6568
[internalFS.realpathCacheKey]: realpathCache,
6669
});
6770
}
6871

72+
/**
73+
* Resolves the path of a given `require` specifier, following symlinks.
74+
* When a custom override is set (e.g. for VFS), it is called first.
75+
* @param {string} requestPath The `require` specifier
76+
* @returns {string}
77+
*/
78+
function toRealPath(requestPath) {
79+
if (customToRealPath !== null) {
80+
return customToRealPath(requestPath, defaultToRealPath);
81+
}
82+
return defaultToRealPath(requestPath);
83+
}
84+
85+
/**
86+
* Set a custom toRealPath override (e.g. for VFS-aware resolution).
87+
* The function receives (requestPath, defaultToRealPath) and should
88+
* return the resolved real path.
89+
* @param {Function|null} fn The custom function, or null to reset
90+
*/
91+
function setCustomToRealPath(fn) {
92+
customToRealPath = fn;
93+
}
94+
6995
/** @type {Set<string>} */
7096
let cjsConditions;
7197
/** @type {string[]} */
@@ -512,6 +538,7 @@ module.exports = {
512538
makeRequireFunction,
513539
normalizeReferrerURL,
514540
stringify,
541+
setCustomToRealPath,
515542
stripBOM,
516543
toRealPath,
517544
hasStartedUserCJSExecution() {

lib/internal/vfs/module_hooks.js

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -478,24 +478,15 @@ function vfsLoadHook(url, context, nextLoad) {
478478
}
479479

480480
/**
481-
* Install hooks into Module._stat and various fs functions.
482-
* Note: fs and internal modules are required here (not at top level) to avoid
483-
* circular dependencies during bootstrap. This module may be loaded early.
481+
* Install module loading hooks (Module._stat, toRealPath override, ESM hooks).
482+
* These make CJS/ESM module resolution VFS-aware without patching fs globally.
484483
*/
485-
function installHooks() {
486-
if (hooksInstalled) {
487-
return;
488-
}
489-
484+
function installModuleHooks() {
490485
const Module = require('internal/modules/cjs/loader').Module;
491-
const fs = require('fs');
486+
const { setCustomToRealPath } = require('internal/modules/helpers');
492487

493-
// Save originals
488+
// Save original Module._stat
494489
originalStat = Module._stat;
495-
originalReadFileSync = fs.readFileSync;
496-
originalRealpathSync = fs.realpathSync;
497-
originalLstatSync = fs.lstatSync;
498-
originalStatSync = fs.statSync;
499490

500491
// Override Module._stat
501492
// This uses the setter which emits an experimental warning, but that's acceptable
@@ -508,6 +499,37 @@ function installHooks() {
508499
return originalStat(filename);
509500
};
510501

502+
// Set VFS-aware toRealPath override for internal module resolution.
503+
// This is called at runtime (not captured at import time), so it works
504+
// correctly with both CJS toRealPath and ESM finalizeResolution.
505+
setCustomToRealPath(function vfsAwareToRealPath(requestPath, defaultFn) {
506+
const vfsResult = findVFSForRealpath(requestPath);
507+
if (vfsResult !== null) {
508+
return vfsResult.realpath;
509+
}
510+
return defaultFn(requestPath);
511+
});
512+
513+
// Register ESM hooks using Module.registerHooks
514+
Module.registerHooks({
515+
resolve: vfsResolveHook,
516+
load: vfsLoadHook,
517+
});
518+
}
519+
520+
/**
521+
* Install fs patches for user code transparency.
522+
* These make fs.readFileSync('/vfs/path'), fs.statSync, etc. work for user code.
523+
*/
524+
function installFsPatches() {
525+
const fs = require('fs');
526+
527+
// Save originals
528+
originalReadFileSync = fs.readFileSync;
529+
originalRealpathSync = fs.realpathSync;
530+
originalLstatSync = fs.lstatSync;
531+
originalStatSync = fs.statSync;
532+
511533
// Override fs.readFileSync
512534
// We need to be careful to only intercept when VFS should handle the path
513535
fs.readFileSync = function readFileSync(path, options) {
@@ -687,12 +709,20 @@ function installHooks() {
687709
}
688710
return FunctionPrototypeCall(originalPromisesWatch, fsPromises, filename, options);
689711
};
712+
}
690713

691-
// Register ESM hooks using Module.registerHooks
692-
Module.registerHooks({
693-
resolve: vfsResolveHook,
694-
load: vfsLoadHook,
695-
});
714+
/**
715+
* Install all VFS hooks: module loading hooks and fs patches.
716+
* Note: fs and internal modules are required here (not at top level) to avoid
717+
* circular dependencies during bootstrap. This module may be loaded early.
718+
*/
719+
function installHooks() {
720+
if (hooksInstalled) {
721+
return;
722+
}
723+
724+
installModuleHooks();
725+
installFsPatches();
696726

697727
hooksInstalled = true;
698728
}

0 commit comments

Comments
 (0)