Skip to content

Commit 51b033a

Browse files
committed
vfs: integrate with CJS and ESM module loaders
Route loader fs and package.json operations through toggleable wrappers so the VFS can resolve and load modules from mounted paths. When no VFS is mounted, the wrappers take a null-check fast path with zero overhead. Hooks: - loaderStat / loaderReadFile / toRealPath / loaderLegacyMainResolve / loaderGetFormatOfExtensionlessFile in lib/internal/modules/helpers.js, consumed by cjs/loader.js, esm/resolve.js, esm/load.js and esm/get_format.js. - loaderReadPackageJSON / loaderGetNearestParentPackageJSON / loaderGetPackageScopeConfig / loaderGetPackageType, consumed by package_json_reader.js. - setLoaderFsOverrides / setLoaderPackageOverrides install / clear all hooks; clearRealpathCache exposes the helpers.js realpath cache so deregister can flush it. lib/internal/vfs/setup.js installs the overrides on first registerVFS and clears every JS-side loader cache (CJS _pathCache, CJS stat cache, realpath cache, package.json cache) on every deregister. The overrides themselves are uninstalled when the last VFS is removed so the fast path is fully restored. legacyMainResolve / extensionless-format behavior matches the C++ binding; package.json validation matches src/node_modules.cc (silently omit non-string main, throw on non-string name/type, etc). The "DO NOT depend on patchability" warnings in esm/load.js and esm/resolve.js are preserved and now point at node:vfs and module.registerHooks() as the formal hook mechanisms. Tests cover require / import / module-hooks / package.json / cache invalidation / cleanup-cycle scenarios under --experimental-vfs. Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent 39b481b commit 51b033a

14 files changed

Lines changed: 2285 additions & 43 deletions

lib/internal/modules/cjs/loader.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ const kFormat = Symbol('kFormat');
113113

114114
// Set first due to cycle with ESM loader functions.
115115
module.exports = {
116+
clearStatCache,
116117
kModuleSource,
117118
kModuleExport,
118119
kModuleExportNames,
@@ -155,14 +156,14 @@ const {
155156
} = internalBinding('contextify');
156157

157158
const assert = require('internal/assert');
158-
const fs = require('fs');
159159
const path = require('path');
160-
const internalFsBinding = internalBinding('fs');
161160
const { safeGetenv } = internalBinding('credentials');
162161
const {
163162
getCjsConditions,
164163
getCjsConditionsArray,
165164
initializeCjsConditions,
165+
loaderReadFile,
166+
loaderStat,
166167
loadBuiltinModule,
167168
makeRequireFunction,
168169
setHasStartedUserCJSExecution,
@@ -272,14 +273,24 @@ function stat(filename) {
272273
const result = statCache.get(filename);
273274
if (result !== undefined) { return result; }
274275
}
275-
const result = internalFsBinding.internalModuleStat(filename);
276+
const result = loaderStat(filename);
276277
if (statCache !== null && result >= 0) {
277278
// Only set cache when `internalModuleStat(filename)` succeeds.
278279
statCache.set(filename, result);
279280
}
280281
return result;
281282
}
282283

284+
/**
285+
* Clear the stat cache. Called when VFS instances are unmounted
286+
* to prevent stale stat results from being returned.
287+
*/
288+
function clearStatCache() {
289+
if (statCache !== null) {
290+
statCache = new SafeMap();
291+
}
292+
}
293+
283294
let _stat = stat;
284295
ObjectDefineProperty(Module, '_stat', {
285296
__proto__: null,
@@ -1201,7 +1212,7 @@ function defaultLoadImpl(filename, format) {
12011212
case 'module-typescript':
12021213
case 'commonjs-typescript':
12031214
case 'typescript': {
1204-
return fs.readFileSync(filename, 'utf8');
1215+
return loaderReadFile(filename, 'utf8');
12051216
}
12061217
case 'builtin':
12071218
return null;

lib/internal/modules/esm/get_format.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ const {
1010
} = primordials;
1111
const { getOptionValue } = require('internal/options');
1212
const { getValidatedPath } = require('internal/fs/utils');
13-
const fsBindings = internalBinding('fs');
1413
const { internal: internalConstants } = internalBinding('constants');
1514

1615
const extensionFormatMap = {
@@ -59,7 +58,8 @@ function mimeToFormat(mime) {
5958
*/
6059
function getFormatOfExtensionlessFile(url) {
6160
const path = getValidatedPath(url);
62-
switch (fsBindings.getFormatOfExtensionlessFile(path)) {
61+
const { loaderGetFormatOfExtensionlessFile } = require('internal/modules/helpers');
62+
switch (loaderGetFormatOfExtensionlessFile(path)) {
6363
case internalConstants.EXTENSIONLESS_FORMAT_WASM:
6464
return 'wasm';
6565
default:

lib/internal/modules/esm/load.js

Lines changed: 7 additions & 5 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 fs = require('fs');
12+
const { loaderReadFile } = require('internal/modules/helpers');
1313

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

@@ -34,11 +34,13 @@ function getSourceSync(url, context) {
3434
const responseURL = href;
3535
let source;
3636
if (protocol === 'file:') {
37-
// If you are reading this code to figure out how to patch Node.js module loading
38-
// behavior - DO NOT depend on the patchability in new code: Node.js
37+
// If you are reading this code to figure out how to patch Node.js module
38+
// loading behavior - DO NOT depend on the patchability in new code: Node.js
3939
// internals may stop going through the JavaScript fs module entirely.
40-
// Prefer module.registerHooks() or other more formal fs hooks released in the future.
41-
source = fs.readFileSync(url);
40+
// Prefer module.registerHooks(), node:vfs, or other more formal fs hooks
41+
// released in the future. loaderReadFile is the toggleable hook used by
42+
// node:vfs and is not part of the public API.
43+
source = loaderReadFile(url);
4244
} else if (protocol === 'data:') {
4345
const result = dataURLProcessor(url);
4446
if (result === 'failure') {

lib/internal/modules/esm/resolve.js

Lines changed: 9 additions & 15 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,16 +22,13 @@ 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 fs = 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');
3229
const { URL, pathToFileURL, fileURLToPath, isURL, URLParse } = require('internal/url');
3330
const { getCWDURL, setOwnProperty } = require('internal/util');
3431
const { canParse: URLCanParse } = internalBinding('url');
35-
const { legacyMainResolve: FSLegacyMainResolve } = internalBinding('fs');
3632
const {
3733
ERR_INPUT_TYPE_NOT_ALLOWED,
3834
ERR_INVALID_ARG_TYPE,
@@ -49,7 +45,7 @@ const {
4945
const { defaultGetFormatWithoutErrors } = require('internal/modules/esm/get_format');
5046
const { getConditionsSet } = require('internal/modules/esm/utils');
5147
const packageJsonReader = require('internal/modules/package_json_reader');
52-
const internalFsBinding = internalBinding('fs');
48+
const { loaderLegacyMainResolve, loaderStat, toRealPath } = require('internal/modules/helpers');
5349

5450
/**
5551
* @typedef {import('internal/modules/esm/package_config.js').PackageConfig} PackageConfig
@@ -149,8 +145,6 @@ function emitLegacyIndexDeprecation(url, path, pkgPath, base, main) {
149145
}
150146
}
151147

152-
const realpathCache = new SafeMap();
153-
154148
const legacyMainResolveExtensions = [
155149
'',
156150
'.js',
@@ -198,7 +192,7 @@ function legacyMainResolve(packageJSONUrl, packageConfig, base) {
198192

199193
const baseStringified = isURL(base) ? base.href : base;
200194

201-
const resolvedOption = FSLegacyMainResolve(pkgPath, packageConfig.main, baseStringified);
195+
const resolvedOption = loaderLegacyMainResolve(pkgPath, packageConfig.main, baseStringified);
202196

203197
const maybeMain = resolvedOption <= legacyMainResolveExtensionsIndexes.kResolvedByMainIndexNode ?
204198
packageConfig.main || './' : '';
@@ -244,7 +238,7 @@ function finalizeResolution(resolved, base, preserveSymlinks) {
244238
throw err;
245239
}
246240

247-
const stats = internalFsBinding.internalModuleStat(
241+
const stats = loaderStat(
248242
StringPrototypeEndsWith(path, '/') ? StringPrototypeSlice(path, -1) : path,
249243
);
250244

@@ -273,13 +267,13 @@ function finalizeResolution(resolved, base, preserveSymlinks) {
273267
}
274268

275269
if (!preserveSymlinks) {
276-
// If you are reading this code to figure out how to patch Node.js module loading
277-
// behavior - DO NOT depend on the patchability in new code: Node.js
270+
// If you are reading this code to figure out how to patch Node.js module
271+
// loading behavior - DO NOT depend on the patchability in new code: Node.js
278272
// internals may stop going through the JavaScript fs module entirely.
279-
// Prefer module.registerHooks() or other more formal fs hooks released in the future.
280-
const real = fs.realpathSync(path, {
281-
[internalFS.realpathCacheKey]: realpathCache,
282-
});
273+
// Prefer module.registerHooks(), node:vfs, or other more formal fs hooks
274+
// released in the future. toRealPath is the toggleable hook used by
275+
// node:vfs and is not part of the public API.
276+
const real = toRealPath(path);
283277
const { search, hash } = resolved;
284278
resolved =
285279
pathToFileURL(real + (StringPrototypeEndsWith(path, sep) ? '/' : ''));

0 commit comments

Comments
 (0)