Skip to content

Commit 02d2624

Browse files
robertsLandoclaude
andauthored
fix: patch all async fs methods (callback + promises) for VFS interception (#10)
* fix: add fs.promises and fs.access/accessSync patches to module hooks The VFS module hooks only patched synchronous fs methods, leaving async operations (fs.promises.*, fs.access) unpatched. This caused ENOENT errors when application code used: - `await fs.promises.access(path)` (e.g., file existence checks) - `await fs.promises.readFile(path)` (e.g., loading templates) - `await fs.promises.stat(path)`, `lstat`, `readdir`, `readlink`, `realpath` - `fs.accessSync(path)` or `fs.access(path, callback)` Since `require('fs/promises')` returns the same object as `fs.promises`, patching directly on `fs.promises` covers both import patterns. Fixes: #8 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add callback fs.stat, fs.lstat, fs.readFile, and fs.createReadStream patches The VFS module hooks only patched sync and promise-based fs methods, leaving callback versions unpatched. This caused ENOENT errors when libraries like express.static (via send) use fs.stat(path, callback). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add callback fs.readdir, fs.readlink, and fs.realpath patches Libraries like glob, graceful-fs, and enhanced-resolve use the callback forms of these methods. Without patches they bypass the VFS entirely. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3e576f7 commit 02d2624

2 files changed

Lines changed: 634 additions & 0 deletions

File tree

lib/module_hooks.js

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,6 +1175,289 @@ function installFsPatches() {
11751175
return originalExistsSync.call(fs, path);
11761176
};
11771177

1178+
// Patch fs.readlinkSync for VFS
1179+
const originalReadlinkSync = fs.readlinkSync;
1180+
fs.readlinkSync = function readlinkSync(path, options) {
1181+
if (typeof path === 'string') {
1182+
const vfsResult = findVFSForRealpath(path);
1183+
if (vfsResult !== null) {
1184+
return vfsResult.realpath;
1185+
}
1186+
}
1187+
return originalReadlinkSync.call(fs, path, options);
1188+
};
1189+
1190+
// --- Async callback patches (fs.access, fs.accessSync) ---
1191+
1192+
const originalAccessSync = fs.accessSync;
1193+
fs.accessSync = function accessSync(path, mode) {
1194+
if (typeof path === 'string') {
1195+
const vfsResult = findVFSForExists(path);
1196+
if (vfsResult !== null) {
1197+
if (!vfsResult.exists) {
1198+
throw createENOENT('access', path);
1199+
}
1200+
return;
1201+
}
1202+
}
1203+
return originalAccessSync.call(fs, path, mode);
1204+
};
1205+
1206+
const originalAccess = fs.access;
1207+
fs.access = function access(path, mode, callback) {
1208+
if (typeof mode === 'function') {
1209+
callback = mode;
1210+
mode = fs.constants.F_OK;
1211+
}
1212+
if (typeof path === 'string') {
1213+
const vfsResult = findVFSForExists(path);
1214+
if (vfsResult !== null) {
1215+
const err = vfsResult.exists ? null : createENOENT('access', path);
1216+
if (callback) process.nextTick(callback, err);
1217+
return;
1218+
}
1219+
}
1220+
return originalAccess.call(fs, path, mode, callback);
1221+
};
1222+
1223+
// --- Callback patches for fs.stat, fs.lstat, fs.readFile, fs.createReadStream ---
1224+
1225+
const originalStat = fs.stat;
1226+
fs.stat = function stat(path, options, callback) {
1227+
if (typeof options === 'function') {
1228+
callback = options;
1229+
options = undefined;
1230+
}
1231+
if (typeof path === 'string') {
1232+
try {
1233+
const vfsResult = findVFSForFsStat(path);
1234+
if (vfsResult !== null) {
1235+
if (callback) process.nextTick(callback, null, vfsResult.stats);
1236+
return;
1237+
}
1238+
} catch (err) {
1239+
if (callback) process.nextTick(callback, err);
1240+
return;
1241+
}
1242+
}
1243+
return originalStat.call(fs, path, options, callback);
1244+
};
1245+
1246+
const originalLstat = fs.lstat;
1247+
fs.lstat = function lstat(path, options, callback) {
1248+
if (typeof options === 'function') {
1249+
callback = options;
1250+
options = undefined;
1251+
}
1252+
if (typeof path === 'string') {
1253+
try {
1254+
const vfsResult = findVFSForFsStat(path);
1255+
if (vfsResult !== null) {
1256+
if (callback) process.nextTick(callback, null, vfsResult.stats);
1257+
return;
1258+
}
1259+
} catch (err) {
1260+
if (callback) process.nextTick(callback, err);
1261+
return;
1262+
}
1263+
}
1264+
return originalLstat.call(fs, path, options, callback);
1265+
};
1266+
1267+
const originalReadFile = fs.readFile;
1268+
fs.readFile = function readFile(path, options, callback) {
1269+
if (typeof options === 'function') {
1270+
callback = options;
1271+
options = undefined;
1272+
}
1273+
if (typeof path === 'string') {
1274+
try {
1275+
const vfsResult = findVFSForRead(path, options);
1276+
if (vfsResult !== null) {
1277+
if (callback) process.nextTick(callback, null, vfsResult.content);
1278+
return;
1279+
}
1280+
} catch (err) {
1281+
if (callback) process.nextTick(callback, err);
1282+
return;
1283+
}
1284+
}
1285+
return originalReadFile.call(fs, path, options, callback);
1286+
};
1287+
1288+
const originalCreateReadStream = fs.createReadStream;
1289+
fs.createReadStream = function createReadStream(path, options) {
1290+
if (typeof path === 'string') {
1291+
try {
1292+
const vfsResult = findVFSForRead(path, options);
1293+
if (vfsResult !== null) {
1294+
const { Readable } = require('node:stream');
1295+
const stream = new Readable({ read() {} });
1296+
stream.push(vfsResult.content);
1297+
stream.push(null);
1298+
return stream;
1299+
}
1300+
} catch (err) {
1301+
const { Readable } = require('node:stream');
1302+
const stream = new Readable({ read() {} });
1303+
process.nextTick(() => stream.destroy(err));
1304+
return stream;
1305+
}
1306+
}
1307+
return originalCreateReadStream.call(fs, path, options);
1308+
};
1309+
1310+
const originalReaddir = fs.readdir;
1311+
fs.readdir = function readdir(path, options, callback) {
1312+
if (typeof options === 'function') {
1313+
callback = options;
1314+
options = undefined;
1315+
}
1316+
if (typeof path === 'string') {
1317+
try {
1318+
const vfsResult = findVFSForReaddir(path, options);
1319+
if (vfsResult !== null) {
1320+
if (callback) process.nextTick(callback, null, vfsResult.entries);
1321+
return;
1322+
}
1323+
} catch (err) {
1324+
if (callback) process.nextTick(callback, err);
1325+
return;
1326+
}
1327+
}
1328+
return originalReaddir.call(fs, path, options, callback);
1329+
};
1330+
1331+
const originalReadlink = fs.readlink;
1332+
fs.readlink = function readlink(path, options, callback) {
1333+
if (typeof options === 'function') {
1334+
callback = options;
1335+
options = undefined;
1336+
}
1337+
if (typeof path === 'string') {
1338+
try {
1339+
const vfsResult = findVFSForRealpath(path);
1340+
if (vfsResult !== null) {
1341+
if (callback) process.nextTick(callback, null, vfsResult.realpath);
1342+
return;
1343+
}
1344+
} catch (err) {
1345+
if (callback) process.nextTick(callback, err);
1346+
return;
1347+
}
1348+
}
1349+
return originalReadlink.call(fs, path, options, callback);
1350+
};
1351+
1352+
const originalRealpath = fs.realpath;
1353+
fs.realpath = function realpath(path, options, callback) {
1354+
if (typeof options === 'function') {
1355+
callback = options;
1356+
options = undefined;
1357+
}
1358+
if (typeof path === 'string') {
1359+
try {
1360+
const vfsResult = findVFSForRealpath(path);
1361+
if (vfsResult !== null) {
1362+
if (callback) process.nextTick(callback, null, vfsResult.realpath);
1363+
return;
1364+
}
1365+
} catch (err) {
1366+
if (callback) process.nextTick(callback, err);
1367+
return;
1368+
}
1369+
}
1370+
return originalRealpath.call(fs, path, options, callback);
1371+
};
1372+
if (originalRealpath.native) {
1373+
fs.realpath.native = originalRealpath.native;
1374+
}
1375+
1376+
// --- fs.promises patches ---
1377+
// Patched directly on the shared object so require('fs/promises') also
1378+
// picks up the changes (it returns the same reference as fs.promises).
1379+
// Fixes: https://github.com/platformatic/vfs/issues/8
1380+
1381+
const origPAccess = fs.promises.access;
1382+
fs.promises.access = async function access(path, mode) {
1383+
if (typeof path === 'string') {
1384+
const vfsResult = findVFSForExists(path);
1385+
if (vfsResult !== null) {
1386+
if (!vfsResult.exists) {
1387+
throw createENOENT('access', path);
1388+
}
1389+
return;
1390+
}
1391+
}
1392+
return origPAccess.call(fs.promises, path, mode);
1393+
};
1394+
1395+
const origPReadFile = fs.promises.readFile;
1396+
fs.promises.readFile = async function readFile(path, options) {
1397+
if (typeof path === 'string') {
1398+
const vfsResult = findVFSForRead(path, options);
1399+
if (vfsResult !== null) {
1400+
return vfsResult.content;
1401+
}
1402+
}
1403+
return origPReadFile.call(fs.promises, path, options);
1404+
};
1405+
1406+
const origPStat = fs.promises.stat;
1407+
fs.promises.stat = async function stat(path, options) {
1408+
if (typeof path === 'string') {
1409+
const vfsResult = findVFSForFsStat(path);
1410+
if (vfsResult !== null) {
1411+
return vfsResult.stats;
1412+
}
1413+
}
1414+
return origPStat.call(fs.promises, path, options);
1415+
};
1416+
1417+
const origPLstat = fs.promises.lstat;
1418+
fs.promises.lstat = async function lstat(path, options) {
1419+
if (typeof path === 'string') {
1420+
const vfsResult = findVFSForFsStat(path);
1421+
if (vfsResult !== null) {
1422+
return vfsResult.stats;
1423+
}
1424+
}
1425+
return origPLstat.call(fs.promises, path, options);
1426+
};
1427+
1428+
const origPReaddir = fs.promises.readdir;
1429+
fs.promises.readdir = async function readdir(path, options) {
1430+
if (typeof path === 'string') {
1431+
const vfsResult = findVFSForReaddir(path, options);
1432+
if (vfsResult !== null) {
1433+
return vfsResult.entries;
1434+
}
1435+
}
1436+
return origPReaddir.call(fs.promises, path, options);
1437+
};
1438+
1439+
const origPReadlink = fs.promises.readlink;
1440+
fs.promises.readlink = async function readlink(path, options) {
1441+
if (typeof path === 'string') {
1442+
const vfsResult = findVFSForRealpath(path);
1443+
if (vfsResult !== null) {
1444+
return vfsResult.realpath;
1445+
}
1446+
}
1447+
return origPReadlink.call(fs.promises, path, options);
1448+
};
1449+
1450+
const origPRealpath = fs.promises.realpath;
1451+
fs.promises.realpath = async function realpath(path, options) {
1452+
if (typeof path === 'string') {
1453+
const vfsResult = findVFSForRealpath(path);
1454+
if (vfsResult !== null) {
1455+
return vfsResult.realpath;
1456+
}
1457+
}
1458+
return origPRealpath.call(fs.promises, path, options);
1459+
};
1460+
11781461
originalWatch = fs.watch;
11791462
fs.watch = function watch(filename, options, listener) {
11801463
if (typeof options === 'function') {

0 commit comments

Comments
 (0)