Skip to content

Commit 632979a

Browse files
committed
vfs: replace fs monkey-patching with toggleable loader wrappers
Add loaderStat(), loaderReadFile(), and setLoaderFsOverrides() to helpers.js, and modify toRealPath() to support a VFS toggle. Replace direct internalFsBinding.internalModuleStat() and fs.readFileSync() calls in the CJS loader, ESM resolver, ESM loader, translators, and package_json_reader with these wrappers. The VFS module_hooks.js now calls setLoaderFsOverrides() first in installHooks(), making loader fs interception order-independent and eliminating conflicts with cached fs method references. Fix two pre-existing bugs in esm/resolve.js finalizeResolution(): - StringPrototypeEndsWith() was called with internalFsBinding as first arg instead of path - StringPrototypeSlice(path, -1) returned the last char instead of stripping the trailing slash (now correctly uses path, 0, -1) Existing fs patches for user-facing operations are kept unchanged.
1 parent d78f4f9 commit 632979a

File tree

7 files changed

+93
-22
lines changed

7 files changed

+93
-22
lines changed

lib/internal/modules/cjs/loader.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,14 @@ const {
155155
} = internalBinding('contextify');
156156

157157
const assert = require('internal/assert');
158-
const fs = require('fs');
159158
const path = require('path');
160-
const internalFsBinding = internalBinding('fs');
161159
const { safeGetenv } = internalBinding('credentials');
162160
const {
163161
getCjsConditions,
164162
getCjsConditionsArray,
165163
initializeCjsConditions,
164+
loaderReadFile,
165+
loaderStat,
166166
loadBuiltinModule,
167167
makeRequireFunction,
168168
setHasStartedUserCJSExecution,
@@ -272,7 +272,7 @@ function stat(filename) {
272272
const result = statCache.get(filename);
273273
if (result !== undefined) { return result; }
274274
}
275-
const result = internalFsBinding.internalModuleStat(filename);
275+
const result = loaderStat(filename);
276276
if (statCache !== null && result >= 0) {
277277
// Only set cache when `internalModuleStat(filename)` succeeds.
278278
statCache.set(filename, result);
@@ -1164,7 +1164,7 @@ function defaultLoadImpl(filename, format) {
11641164
case 'module-typescript':
11651165
case 'commonjs-typescript':
11661166
case 'typescript': {
1167-
return fs.readFileSync(filename, 'utf8');
1167+
return loaderReadFile(filename, 'utf8');
11681168
}
11691169
case 'builtin':
11701170
return null;

lib/internal/modules/esm/load.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const {
99

1010
const { defaultGetFormat } = require('internal/modules/esm/get_format');
1111
const { validateAttributes, emitImportAssertionWarning } = require('internal/modules/esm/assert');
12-
const { readFileSync } = require('fs');
12+
const { loaderReadFile } = require('internal/modules/helpers');
1313

1414
const { Buffer: { from: BufferFrom } } = require('buffer');
1515

@@ -34,7 +34,7 @@ function getSourceSync(url, context) {
3434
const responseURL = href;
3535
let source;
3636
if (protocol === 'file:') {
37-
source = readFileSync(url);
37+
source = loaderReadFile(url);
3838
} else if (protocol === 'data:') {
3939
const result = dataURLProcessor(url);
4040
if (result === 'failure') {

lib/internal/modules/esm/resolve.js

Lines changed: 4 additions & 11 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,7 @@ 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');
2926
const { getOptionValue } = require('internal/options');
3027
// Do not eagerly grab .manifest, it may be in TDZ
3128
const { sep, posix: { relative: relativePosixPath }, resolve } = require('path');
@@ -49,7 +46,7 @@ const {
4946
const { defaultGetFormatWithoutErrors } = require('internal/modules/esm/get_format');
5047
const { getConditionsSet } = require('internal/modules/esm/utils');
5148
const packageJsonReader = require('internal/modules/package_json_reader');
52-
const internalFsBinding = internalBinding('fs');
49+
const { loaderStat, toRealPath } = require('internal/modules/helpers');
5350

5451
/**
5552
* @typedef {import('internal/modules/esm/package_config.js').PackageConfig} PackageConfig
@@ -149,8 +146,6 @@ function emitLegacyIndexDeprecation(url, path, pkgPath, base, main) {
149146
}
150147
}
151148

152-
const realpathCache = new SafeMap();
153-
154149
const legacyMainResolveExtensions = [
155150
'',
156151
'.js',
@@ -244,8 +239,8 @@ function finalizeResolution(resolved, base, preserveSymlinks) {
244239
throw err;
245240
}
246241

247-
const stats = internalFsBinding.internalModuleStat(
248-
StringPrototypeEndsWith(internalFsBinding, path, '/') ? StringPrototypeSlice(path, -1) : path,
242+
const stats = loaderStat(
243+
StringPrototypeEndsWith(path, '/') ? StringPrototypeSlice(path, 0, -1) : path,
249244
);
250245

251246
// Check for stats.isDirectory()
@@ -273,9 +268,7 @@ function finalizeResolution(resolved, base, preserveSymlinks) {
273268
}
274269

275270
if (!preserveSymlinks) {
276-
const real = realpathSync(path, {
277-
[internalFS.realpathCacheKey]: realpathCache,
278-
});
271+
const real = toRealPath(path);
279272
const { search, hash } = resolved;
280273
resolved =
281274
pathToFileURL(real + (StringPrototypeEndsWith(path, sep) ? '/' : ''));

lib/internal/modules/esm/translators.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ const {
2222

2323
const { BuiltinModule } = require('internal/bootstrap/realm');
2424
const assert = require('internal/assert');
25-
const { readFileSync } = require('fs');
2625
const { dirname, extname } = require('path');
2726
const {
2827
assertBufferSource,
28+
loaderReadFile,
2929
loadBuiltinModule,
3030
stringify,
3131
stripBOM,
@@ -341,7 +341,7 @@ translators.set('commonjs', function commonjsStrategy(url, translateContext, par
341341

342342
try {
343343
// We still need to read the FS to detect the exports.
344-
translateContext.source ??= readFileSync(new URL(url), 'utf8');
344+
translateContext.source ??= loaderReadFile(new URL(url), 'utf8');
345345
} catch {
346346
// Continue regardless of error.
347347
}

lib/internal/modules/helpers.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const { emitWarningSync } = require('internal/process/warning');
3434
const lazyTmpdir = getLazy(() => require('os').tmpdir());
3535
const { join } = path;
3636

37+
const internalFsBinding = internalBinding('fs');
3738
const { canParse: URLCanParse } = internalBinding('url');
3839
const {
3940
enableCompileCache: _enableCompileCache,
@@ -56,12 +57,56 @@ let debug = require('internal/util/debuglog').debuglog('module', (fn) => {
5657
* @type {Map<string, string>}
5758
*/
5859
const realpathCache = new SafeMap();
60+
// Toggleable loader fs overrides for VFS support.
61+
// When null, the fast path (no VFS) is taken with zero overhead.
62+
let _loaderStat = null;
63+
let _loaderReadFile = null;
64+
let _loaderRealpath = null;
65+
66+
/**
67+
* Set override functions for the module loader's fs operations.
68+
* @param {{ stat?: Function, readFile?: Function, realpath?: Function }} overrides
69+
*/
70+
function setLoaderFsOverrides({ stat, readFile, realpath }) {
71+
_loaderStat = stat;
72+
_loaderReadFile = readFile;
73+
_loaderRealpath = realpath;
74+
}
75+
76+
/**
77+
* Wrapper for internalModuleStat that supports VFS toggle.
78+
* @param {string} filename Absolute path to stat
79+
* @returns {number}
80+
*/
81+
function loaderStat(filename) {
82+
if (_loaderStat !== null) { return _loaderStat(filename); }
83+
return internalFsBinding.internalModuleStat(filename);
84+
}
85+
86+
/**
87+
* Wrapper for fs.readFileSync that supports VFS toggle.
88+
* @param {string|URL} filename Path to read
89+
* @param {string|object} options Read options
90+
* @returns {string|Buffer}
91+
*/
92+
function loaderReadFile(filename, options) {
93+
if (_loaderReadFile !== null) {
94+
const result = _loaderReadFile(filename, options);
95+
if (result !== undefined) { return result; }
96+
}
97+
return fs.readFileSync(filename, options);
98+
}
99+
59100
/**
60101
* Resolves the path of a given `require` specifier, following symlinks.
61102
* @param {string} requestPath The `require` specifier
62103
* @returns {string}
63104
*/
64105
function toRealPath(requestPath) {
106+
if (_loaderRealpath !== null) {
107+
const result = _loaderRealpath(requestPath);
108+
if (result !== undefined) { return result; }
109+
}
65110
return fs.realpathSync(requestPath, {
66111
[internalFS.realpathCacheKey]: realpathCache,
67112
});
@@ -524,10 +569,13 @@ module.exports = {
524569
getCjsConditionsArray,
525570
getCompileCacheDir,
526571
initializeCjsConditions,
572+
loaderReadFile,
573+
loaderStat,
527574
loadBuiltinModuleForEmbedder,
528575
loadBuiltinModule,
529576
makeRequireFunction,
530577
normalizeReferrerURL,
578+
setLoaderFsOverrides,
531579
stringify,
532580
stripBOM,
533581
toRealPath,

lib/internal/modules/package_json_reader.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const { kEmptyObject } = require('internal/util');
2727
const modulesBinding = internalBinding('modules');
2828
const path = require('path');
2929
const { validateString } = require('internal/validators');
30-
const internalFsBinding = internalBinding('fs');
30+
const { loaderStat } = require('internal/modules/helpers');
3131

3232

3333
/**
@@ -280,7 +280,7 @@ function getPackageJSONURL(specifier, base) {
280280
let packageJSONPath = fileURLToPath(packageJSONUrl);
281281
let lastPath;
282282
do {
283-
const stat = internalFsBinding.internalModuleStat(
283+
const stat = loaderStat(
284284
StringPrototypeSlice(packageJSONPath, 0, packageJSONPath.length - 13),
285285
);
286286
// Check for !stat.isDirectory()

lib/internal/vfs/module_hooks.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const {
88
FunctionPrototypeCall,
99
JSONParse,
1010
ObjectGetOwnPropertyNames,
11+
String,
1112
StringPrototypeEndsWith,
1213
StringPrototypeIndexOf,
1314
StringPrototypeSlice,
@@ -1071,13 +1072,42 @@ function installFsPatches() {
10711072
}
10721073

10731074
/**
1074-
* Install all VFS hooks: module loading hooks and fs patches.
1075+
* Install toggleable loader overrides so that the module loader's
1076+
* internal fs operations (stat, readFile, realpath) are redirected
1077+
* to VFS when appropriate. This is order-independent - unlike fs
1078+
* monkey-patches, it works even when the loader has already cached
1079+
* references to the original fs methods.
1080+
*/
1081+
function installModuleLoaderOverrides() {
1082+
const { setLoaderFsOverrides } = require('internal/modules/helpers');
1083+
setLoaderFsOverrides({
1084+
stat(filename) {
1085+
const result = findVFSForStat(filename);
1086+
if (result !== null) return result.result;
1087+
return internalBinding('fs').internalModuleStat(filename);
1088+
},
1089+
readFile(filename, options) {
1090+
const pathStr = typeof filename === 'string' ? filename :
1091+
(filename instanceof URL ? fileURLToPath(filename) : String(filename));
1092+
const result = findVFSForRead(pathStr, options);
1093+
return result !== null ? result.content : undefined;
1094+
},
1095+
realpath(filename) {
1096+
const result = findVFSForRealpath(filename);
1097+
return result !== null ? result.realpath : undefined;
1098+
},
1099+
});
1100+
}
1101+
1102+
/**
1103+
* Install all VFS hooks: loader overrides, module hooks, and fs patches.
10751104
*/
10761105
function installHooks() {
10771106
if (hooksInstalled) {
10781107
return;
10791108
}
10801109

1110+
installModuleLoaderOverrides();
10811111
installModuleHooks();
10821112
installFsPatches();
10831113

0 commit comments

Comments
 (0)