Skip to content

Commit b931747

Browse files
committed
vfs: extend toggleable wrappers to readFileSync and internalModuleStat
ESM files capture fs methods at import time before VFS patches are applied. Extend the toggle pattern (used for toRealPath) to readFileSync and internalModuleStat so VFS can intercept these calls in the ESM loader. - Add readFileSync/setCustomReadFileSync wrappers to helpers.js - Add internalModuleStat/setCustomInternalModuleStat wrappers - Update esm/load.js and esm/translators.js to use helpers.readFileSync - Update esm/resolve.js to use helpers.internalModuleStat - Fix pre-existing bug: StringPrototypeEndsWith received wrong first arg - Revert inline extensionFormatMap, import from esm/formats instead - Register new VFS overrides in module_hooks.js installModuleHooks() - Add buffer independence test for SEA VFS - Document C++ package.json reader gap for VFS packages
1 parent dd13aff commit b931747

File tree

6 files changed

+125
-15
lines changed

6 files changed

+125
-15
lines changed

lib/internal/modules/esm/load.js

Lines changed: 1 addition & 1 deletion
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 { readFileSync } = require('internal/modules/helpers');
1313

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

lib/internal/modules/esm/resolve.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const {
2323
} = primordials;
2424
const assert = require('internal/assert');
2525
const { BuiltinModule } = require('internal/bootstrap/realm');
26-
const { toRealPath } = require('internal/modules/helpers');
26+
const { toRealPath, internalModuleStat } = require('internal/modules/helpers');
2727
const { getOptionValue } = require('internal/options');
2828
// Do not eagerly grab .manifest, it may be in TDZ
2929
const { sep, posix: { relative: relativePosixPath }, resolve } = require('path');
@@ -47,7 +47,6 @@ const {
4747
const { defaultGetFormatWithoutErrors } = require('internal/modules/esm/get_format');
4848
const { getConditionsSet } = require('internal/modules/esm/utils');
4949
const packageJsonReader = require('internal/modules/package_json_reader');
50-
const internalFsBinding = internalBinding('fs');
5150

5251
/**
5352
* @typedef {import('internal/modules/esm/package_config.js').PackageConfig} PackageConfig
@@ -240,8 +239,8 @@ function finalizeResolution(resolved, base, preserveSymlinks) {
240239
throw err;
241240
}
242241

243-
const stats = internalFsBinding.internalModuleStat(
244-
StringPrototypeEndsWith(internalFsBinding, path, '/') ? StringPrototypeSlice(path, -1) : path,
242+
const stats = internalModuleStat(
243+
StringPrototypeEndsWith(path, '/') ? StringPrototypeSlice(path, -1) : path,
245244
);
246245

247246
// Check for stats.isDirectory()

lib/internal/modules/esm/translators.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ 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,
2928
loadBuiltinModule,
29+
readFileSync,
3030
stringify,
3131
stripBOM,
3232
urlToFilename,

lib/internal/modules/helpers.js

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,75 @@ function setCustomToRealPath(fn) {
9292
customToRealPath = fn;
9393
}
9494

95+
let customReadFileSync = null;
96+
97+
/**
98+
* Default implementation: reads a file from disk.
99+
* @param {string} path The file path
100+
* @param {string|object} options Read options
101+
* @returns {string|Buffer}
102+
*/
103+
function defaultReadFileSync(path, options) {
104+
return fs.readFileSync(path, options);
105+
}
106+
107+
/**
108+
* Reads a file, delegating to a custom override when set (e.g. for VFS).
109+
* @param {string} path The file path
110+
* @param {string|object} options Read options
111+
* @returns {string|Buffer}
112+
*/
113+
function readFileSync(path, options) {
114+
if (customReadFileSync !== null) {
115+
return customReadFileSync(path, options, defaultReadFileSync);
116+
}
117+
return defaultReadFileSync(path, options);
118+
}
119+
120+
/**
121+
* Set a custom readFileSync override (e.g. for VFS-aware reading).
122+
* The function receives (path, options, defaultReadFileSync) and should
123+
* return the file content.
124+
* @param {Function|null} fn The custom function, or null to reset
125+
*/
126+
function setCustomReadFileSync(fn) {
127+
customReadFileSync = fn;
128+
}
129+
130+
const internalFsBinding = internalBinding('fs');
131+
let customInternalModuleStat = null;
132+
133+
/**
134+
* Default implementation: calls the C++ internalModuleStat binding.
135+
* @param {string} path The file path
136+
* @returns {number} 0 for file, 1 for directory, negative for not found
137+
*/
138+
function defaultInternalModuleStat(path) {
139+
return internalFsBinding.internalModuleStat(path);
140+
}
141+
142+
/**
143+
* Checks the stat of a module path, delegating to a custom override when set.
144+
* @param {string} path The file path
145+
* @returns {number} 0 for file, 1 for directory, negative for not found
146+
*/
147+
function internalModuleStat(path) {
148+
if (customInternalModuleStat !== null) {
149+
return customInternalModuleStat(path, defaultInternalModuleStat);
150+
}
151+
return defaultInternalModuleStat(path);
152+
}
153+
154+
/**
155+
* Set a custom internalModuleStat override (e.g. for VFS-aware stat).
156+
* The function receives (path, defaultInternalModuleStat) and should
157+
* return the stat result.
158+
* @param {Function|null} fn The custom function, or null to reset
159+
*/
160+
function setCustomInternalModuleStat(fn) {
161+
customInternalModuleStat = fn;
162+
}
163+
95164
/** @type {Set<string>} */
96165
let cjsConditions;
97166
/** @type {string[]} */
@@ -533,12 +602,16 @@ module.exports = {
533602
getCjsConditionsArray,
534603
getCompileCacheDir,
535604
initializeCjsConditions,
605+
internalModuleStat,
536606
loadBuiltinModuleForEmbedder,
537607
loadBuiltinModule,
538608
makeRequireFunction,
539609
normalizeReferrerURL,
540-
stringify,
610+
readFileSync,
611+
setCustomInternalModuleStat,
612+
setCustomReadFileSync,
541613
setCustomToRealPath,
614+
stringify,
542615
stripBOM,
543616
toRealPath,
544617
hasStartedUserCJSExecution() {

lib/internal/vfs/module_hooks.js

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,7 @@ const {
1111
const path = require('path');
1212
const { dirname, extname, isAbsolute, resolve } = path;
1313
const pathPosix = path.posix;
14-
// Inline format map to avoid dependency on internal/modules/esm/formats,
15-
// which may not be available during early bootstrap (e.g. SEA).
16-
// VFS defaults .js to 'commonjs' (see getFormatFromExtension below).
17-
const extensionFormatMap = {
18-
'__proto__': null, '.cjs': 'commonjs', '.mjs': 'module',
19-
'.json': 'json', '.wasm': 'wasm',
20-
};
14+
const { extensionFormatMap } = require('internal/modules/esm/formats');
2115

2216
/**
2317
* Normalizes a VFS path. Uses POSIX normalization for Unix-style paths (starting with /)
@@ -489,7 +483,11 @@ function vfsLoadHook(url, context, nextLoad) {
489483
*/
490484
function installModuleHooks() {
491485
const Module = require('internal/modules/cjs/loader').Module;
492-
const { setCustomToRealPath } = require('internal/modules/helpers');
486+
const {
487+
setCustomToRealPath,
488+
setCustomReadFileSync,
489+
setCustomInternalModuleStat,
490+
} = require('internal/modules/helpers');
493491

494492
// Save original Module._stat
495493
originalStat = Module._stat;
@@ -516,6 +514,31 @@ function installModuleHooks() {
516514
return defaultFn(requestPath);
517515
});
518516

517+
// Set VFS-aware readFileSync override for internal module loading.
518+
// ESM load.js and translators.js capture readFileSync at import time,
519+
// so this toggle pattern ensures VFS intercepts those calls.
520+
setCustomReadFileSync(function vfsAwareReadFileSync(path, options, defaultFn) {
521+
if (typeof path === 'string' || path instanceof URL) {
522+
const pathStr = typeof path === 'string' ? path : path.pathname;
523+
const vfsResult = findVFSForRead(pathStr, options);
524+
if (vfsResult !== null) {
525+
return vfsResult.content;
526+
}
527+
}
528+
return defaultFn(path, options);
529+
});
530+
531+
// Set VFS-aware internalModuleStat override for ESM resolution.
532+
// ESM resolve.js captures internalModuleStat at import time,
533+
// so this toggle pattern ensures VFS intercepts those calls.
534+
setCustomInternalModuleStat(function vfsAwareModuleStat(path, defaultFn) {
535+
const vfsResult = findVFSForStat(path);
536+
if (vfsResult !== null) {
537+
return vfsResult.result;
538+
}
539+
return defaultFn(path);
540+
});
541+
519542
// Register ESM hooks using Module.registerHooks
520543
Module.registerHooks({
521544
resolve: vfsResolveHook,

test/fixtures/sea/vfs/sea.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,21 @@ const vfsAsset = fs.readFileSync('/sea/data/greeting.txt', 'utf8');
5555
assert.strictEqual(seaAsset, vfsAsset, 'node:sea and VFS should return the same content');
5656
console.log('node:sea API and VFS coexistence test passed');
5757

58+
// Test buffer independence: multiple reads return independent copies
59+
const buf1 = fs.readFileSync('/sea/data/greeting.txt');
60+
const buf2 = fs.readFileSync('/sea/data/greeting.txt');
61+
const original = buf1[0];
62+
buf1[0] = 0xFF;
63+
assert.strictEqual(buf2[0], original, 'buf2 should be unaffected by buf1 mutation');
64+
assert.strictEqual(buf1[0], 0xFF, 'buf1 mutation should persist');
65+
console.log('buffer independence test passed');
66+
67+
// TODO(mcollina): The CJS module loader reads package.json via C++ binding
68+
// (internalBinding('modules').readPackageJSON), which doesn't go through VFS.
69+
// This means "exports" conditions in package.json won't work for VFS packages.
70+
// The test below works because "main": "index.js" matches the default fallback.
71+
// A follow-up PR should make the C++ package.json reader VFS-aware.
72+
5873
// Test node_modules package lookup via VFS
5974
const testPkg = require('test-pkg');
6075
assert.strictEqual(testPkg.name, 'test-pkg', 'package name should match');

0 commit comments

Comments
 (0)