Skip to content

Commit 386ec3e

Browse files
committed
vfs: move fs interception from monkey-patching into lib/fs.js
Instead of monkey-patching fs.readFileSync, fs.statSync, etc. at runtime in module_hooks.js, add VFS handler guards directly inside the fs method bodies. This fixes the destructuring problem where capturing a reference before vfs.mount() would bypass VFS: const { readFileSync } = require('fs'); vfs.mount('/app'); readFileSync('/app/file.txt'); // now works correctly The approach: - Add shared vfsState object and setVfsHandlers() in internal/fs/utils - Add a null-check guard at the top of 9 sync methods in lib/fs.js and 3 async methods in lib/internal/fs/promises.js - Replace installFsPatches() with a handler object registered via setVfsHandlers() — same logic, no monkey-patching - Rename module_hooks.js to setup.js (no longer does monkey-patching) When no VFS is active, the overhead is a single null comparison per call. When the last VFS is unmounted, handlers are cleared to restore the zero-overhead path.
1 parent 453ca3b commit 386ec3e

File tree

6 files changed

+283
-216
lines changed

6 files changed

+283
-216
lines changed

lib/fs.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ const {
127127
validateRmOptionsSync,
128128
validateRmdirOptions,
129129
validateStringAfterArrayBufferView,
130+
vfsState,
130131
warnOnNonPortableTemplate,
131132
} = require('internal/fs/utils');
132133
const {
@@ -271,6 +272,11 @@ let showExistsDeprecation = true;
271272
* @returns {boolean}
272273
*/
273274
function existsSync(path) {
275+
const h = vfsState.handlers;
276+
if (h !== null) {
277+
const result = h.existsSync(path);
278+
if (result !== undefined) return result;
279+
}
274280
try {
275281
path = getValidatedPath(path);
276282
} catch (err) {
@@ -427,6 +433,11 @@ function tryReadSync(fd, isUserFd, buffer, pos, len) {
427433
* @returns {string | Buffer}
428434
*/
429435
function readFileSync(path, options) {
436+
const h = vfsState.handlers;
437+
if (h !== null) {
438+
const result = h.readFileSync(path, options);
439+
if (result !== undefined) return result;
440+
}
430441
options = getOptions(options, { flag: 'r' });
431442

432443
if (options.encoding === 'utf8' || options.encoding === 'utf-8') {
@@ -1541,6 +1552,11 @@ function readdir(path, options, callback) {
15411552
* @returns {string | Buffer[] | Dirent[]}
15421553
*/
15431554
function readdirSync(path, options) {
1555+
const h = vfsState.handlers;
1556+
if (h !== null) {
1557+
const result = h.readdirSync(path, options);
1558+
if (result !== undefined) return result;
1559+
}
15441560
options = getOptions(options);
15451561
path = getValidatedPath(path);
15461562
if (options.recursive != null) {
@@ -1678,6 +1694,11 @@ function fstatSync(fd, options = { bigint: false }) {
16781694
* @returns {Stats | undefined}
16791695
*/
16801696
function lstatSync(path, options = { bigint: false, throwIfNoEntry: true }) {
1697+
const h = vfsState.handlers;
1698+
if (h !== null) {
1699+
const result = h.lstatSync(path, options);
1700+
if (result !== undefined) return result;
1701+
}
16811702
path = getValidatedPath(path);
16821703
if (permission.isEnabled() && !permission.has('fs.read', path)) {
16831704
const resource = BufferIsBuffer(path) ? BufferToString(path) : path;
@@ -1707,6 +1728,11 @@ function lstatSync(path, options = { bigint: false, throwIfNoEntry: true }) {
17071728
* @returns {Stats}
17081729
*/
17091730
function statSync(path, options = { bigint: false, throwIfNoEntry: true }) {
1731+
const h = vfsState.handlers;
1732+
if (h !== null) {
1733+
const result = h.statSync(path, options);
1734+
if (result !== undefined) return result;
1735+
}
17101736
const stats = binding.stat(
17111737
getValidatedPath(path),
17121738
options.bigint,
@@ -2496,6 +2522,11 @@ function appendFileSync(path, data, options) {
24962522
* @returns {watchers.FSWatcher}
24972523
*/
24982524
function watch(filename, options, listener) {
2525+
const h = vfsState.handlers;
2526+
if (h !== null) {
2527+
const result = h.watch(filename, options, listener);
2528+
if (result !== undefined) return result;
2529+
}
24992530
if (typeof options === 'function') {
25002531
listener = options;
25012532
}
@@ -2563,6 +2594,11 @@ const statWatchers = new SafeMap();
25632594
* @returns {watchers.StatWatcher}
25642595
*/
25652596
function watchFile(filename, options, listener) {
2597+
const h = vfsState.handlers;
2598+
if (h !== null) {
2599+
const result = h.watchFile(filename, options, listener);
2600+
if (result !== undefined) return result;
2601+
}
25662602
filename = getValidatedPath(filename);
25672603
filename = pathModule.resolve(filename);
25682604
let stat;
@@ -2605,6 +2641,10 @@ function watchFile(filename, options, listener) {
26052641
* @returns {void}
26062642
*/
26072643
function unwatchFile(filename, listener) {
2644+
const h = vfsState.handlers;
2645+
if (h !== null) {
2646+
if (h.unwatchFile(filename, listener)) return;
2647+
}
26082648
filename = getValidatedPath(filename);
26092649
filename = pathModule.resolve(filename);
26102650
const stat = statWatchers.get(filename);
@@ -2682,6 +2722,11 @@ if (isWindows) {
26822722
* @returns {string | Buffer}
26832723
*/
26842724
function realpathSync(p, options) {
2725+
const h = vfsState.handlers;
2726+
if (h !== null) {
2727+
const result = h.realpathSync(p, options);
2728+
if (result !== undefined) return result;
2729+
}
26852730
options = getOptions(options);
26862731
p = toPathIfFileURL(p);
26872732
if (typeof p !== 'string') {

lib/internal/fs/promises.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ const {
7070
validateRmOptions,
7171
validateRmdirOptions,
7272
validateStringAfterArrayBufferView,
73+
vfsState,
7374
warnOnNonPortableTemplate,
7475
} = require('internal/fs/utils');
7576
const { opendir } = require('internal/fs/dir');
@@ -941,6 +942,11 @@ async function readdirRecursive(originalPath, options) {
941942
}
942943

943944
async function readdir(path, options) {
945+
const h = vfsState.handlers;
946+
if (h !== null) {
947+
const vfsResult = await h.readdir(path, options);
948+
if (vfsResult !== undefined) return vfsResult;
949+
}
944950
options = getOptions(options);
945951

946952
// Make shallow copy to prevent mutating options from affecting results
@@ -1018,6 +1024,11 @@ async function fstat(handle, options = { bigint: false }) {
10181024
}
10191025

10201026
async function lstat(path, options = { bigint: false }) {
1027+
const h = vfsState.handlers;
1028+
if (h !== null) {
1029+
const vfsResult = await h.lstat(path, options);
1030+
if (vfsResult !== undefined) return vfsResult;
1031+
}
10211032
const result = await PromisePrototypeThen(
10221033
binding.lstat(getValidatedPath(path), options.bigint, kUsePromises),
10231034
undefined,
@@ -1275,6 +1286,14 @@ async function readFile(path, options) {
12751286
}
12761287

12771288
async function* _watch(filename, options = kEmptyObject) {
1289+
const h = vfsState.handlers;
1290+
if (h !== null) {
1291+
const result = h.promisesWatch(filename, options);
1292+
if (result !== undefined) {
1293+
yield* result;
1294+
return;
1295+
}
1296+
}
12781297
validateObject(options, 'options');
12791298

12801299
if (options.recursive != null) {
@@ -1333,7 +1352,7 @@ module.exports = {
13331352
writeFile,
13341353
appendFile,
13351354
readFile,
1336-
watch: !isMacOS && !isWindows ? _watch : watch,
1355+
watch: _watch,
13371356
constants,
13381357
},
13391358

lib/internal/fs/utils.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,11 @@ const validatePosition = hideStackFrames((position, name, length) => {
915915
}
916916
});
917917

918+
// Shared VFS handler state for fs wrapping.
919+
// When handlers is null, no VFS is active (zero overhead).
920+
const vfsState = { __proto__: null, handlers: null };
921+
function setVfsHandlers(handlers) { vfsState.handlers = handlers; }
922+
918923
module.exports = {
919924
constants: {
920925
kIoMaxLength,
@@ -953,4 +958,6 @@ module.exports = {
953958
validateRmdirOptions,
954959
validateStringAfterArrayBufferView,
955960
warnOnNonPortableTemplate,
961+
vfsState,
962+
setVfsHandlers,
956963
};

lib/internal/vfs/file_system.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,15 @@ const kVirtualCwdEnabled = Symbol('kVirtualCwdEnabled');
4444
const kOriginalChdir = Symbol('kOriginalChdir');
4545
const kOriginalCwd = Symbol('kOriginalCwd');
4646

47-
// Lazy-loaded module hooks
47+
// Lazy-loaded VFS setup
4848
let registerVFS;
4949
let deregisterVFS;
5050

51-
function loadModuleHooks() {
51+
function loadVfsSetup() {
5252
if (!registerVFS) {
53-
const hooks = require('internal/vfs/module_hooks');
54-
registerVFS = hooks.registerVFS;
55-
deregisterVFS = hooks.deregisterVFS;
53+
const setup = require('internal/vfs/setup');
54+
registerVFS = setup.registerVFS;
55+
deregisterVFS = setup.deregisterVFS;
5656
}
5757
}
5858

@@ -228,7 +228,7 @@ class VirtualFileSystem {
228228
this[kMountPoint] = resolvePath(prefix);
229229
this[kMounted] = true;
230230
if (this[kModuleHooks]) {
231-
loadModuleHooks();
231+
loadVfsSetup();
232232
registerVFS(this);
233233
}
234234
if (this[kVirtualCwdEnabled]) {
@@ -244,7 +244,7 @@ class VirtualFileSystem {
244244
unmount() {
245245
this.#unhookProcessCwd();
246246
if (this[kModuleHooks]) {
247-
loadModuleHooks();
247+
loadVfsSetup();
248248
deregisterVFS(this);
249249
}
250250
this[kMountPoint] = null;

0 commit comments

Comments
 (0)