Skip to content

Commit 5b25abc

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.
1 parent 6f29e1a commit 5b25abc

13 files changed

Lines changed: 2027 additions & 40 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: 2 additions & 6 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,7 @@ 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
39-
// 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);
37+
source = loaderReadFile(url);
4238
} else if (protocol === 'data:') {
4339
const result = dataURLProcessor(url);
4440
if (result === 'failure') {

lib/internal/modules/esm/resolve.js

Lines changed: 4 additions & 16 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,7 @@ 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
278-
// 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-
});
270+
const real = toRealPath(path);
283271
const { search, hash } = resolved;
284272
resolved =
285273
pathToFileURL(real + (StringPrototypeEndsWith(path, sep) ? '/' : ''));

lib/internal/modules/helpers.js

Lines changed: 163 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,15 @@ 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');
39+
const modulesBinding = internalBinding('modules');
3840
const {
3941
enableCompileCache: _enableCompileCache,
4042
getCompileCacheDir: _getCompileCacheDir,
4143
compileCacheStatus: _compileCacheStatus,
4244
flushCompileCache,
43-
} = internalBinding('modules');
45+
} = modulesBinding;
4446

4547
const lazyCJSLoader = getLazy(() => require('internal/modules/cjs/loader'));
4648
let debug = require('internal/util/debuglog').debuglog('module', (fn) => {
@@ -56,17 +58,167 @@ let debug = require('internal/util/debuglog').debuglog('module', (fn) => {
5658
* @type {Map<string, string>}
5759
*/
5860
const realpathCache = new SafeMap();
61+
// Toggleable loader fs overrides for VFS support.
62+
// When null, the fast path (no VFS) is taken with zero overhead.
63+
let _loaderStat = null;
64+
let _loaderReadFile = null;
65+
let _loaderRealpath = null;
66+
let _loaderLegacyMainResolve = null;
67+
let _loaderGetFormatOfExtensionlessFile = null;
68+
69+
/**
70+
* Set override functions for the module loader's fs operations.
71+
* @param {{ stat?: Function, readFile?: Function, realpath?: Function,
72+
* legacyMainResolve?: Function, getFormatOfExtensionlessFile?: Function }} overrides
73+
*/
74+
function setLoaderFsOverrides({ stat, readFile, realpath, legacyMainResolve, getFormatOfExtensionlessFile }) {
75+
_loaderStat = stat;
76+
_loaderReadFile = readFile;
77+
_loaderRealpath = realpath;
78+
_loaderLegacyMainResolve = legacyMainResolve;
79+
_loaderGetFormatOfExtensionlessFile = getFormatOfExtensionlessFile;
80+
}
81+
82+
/**
83+
* Wrapper for internalModuleStat that supports VFS toggle.
84+
* @param {string} filename Absolute path to stat
85+
* @returns {number}
86+
*/
87+
function loaderStat(filename) {
88+
if (_loaderStat !== null) { return _loaderStat(filename); }
89+
return internalFsBinding.internalModuleStat(filename);
90+
}
91+
92+
/**
93+
* Wrapper for fs.readFileSync that supports VFS toggle.
94+
* @param {string|URL} filename Path to read
95+
* @param {string|object} options Read options
96+
* @returns {string|Buffer}
97+
*/
98+
function loaderReadFile(filename, options) {
99+
if (_loaderReadFile !== null) {
100+
const result = _loaderReadFile(filename, options);
101+
if (result !== undefined) { return result; }
102+
}
103+
return fs.readFileSync(filename, options);
104+
}
105+
59106
/**
60107
* Resolves the path of a given `require` specifier, following symlinks.
61108
* @param {string} requestPath The `require` specifier
62109
* @returns {string}
63110
*/
64111
function toRealPath(requestPath) {
112+
if (_loaderRealpath !== null) {
113+
const result = _loaderRealpath(requestPath);
114+
if (result !== undefined) { return result; }
115+
}
65116
return fs.realpathSync(requestPath, {
66117
[internalFS.realpathCacheKey]: realpathCache,
67118
});
68119
}
69120

121+
/**
122+
* Wrapper for internalBinding('fs').legacyMainResolve that supports VFS toggle.
123+
* @param {string} pkgPath The package directory path
124+
* @param {string} main The package main field
125+
* @param {string} base The base URL string
126+
* @returns {number}
127+
*/
128+
function loaderLegacyMainResolve(pkgPath, main, base) {
129+
if (_loaderLegacyMainResolve !== null) {
130+
const result = _loaderLegacyMainResolve(pkgPath, main, base);
131+
if (result !== undefined) { return result; }
132+
}
133+
return internalFsBinding.legacyMainResolve(pkgPath, main, base);
134+
}
135+
136+
/**
137+
* Wrapper for internalBinding('fs').getFormatOfExtensionlessFile that supports VFS toggle.
138+
* @param {string} path The file path
139+
* @returns {number}
140+
*/
141+
function loaderGetFormatOfExtensionlessFile(path) {
142+
if (_loaderGetFormatOfExtensionlessFile !== null) {
143+
const result = _loaderGetFormatOfExtensionlessFile(path);
144+
if (result !== undefined) { return result; }
145+
}
146+
return internalFsBinding.getFormatOfExtensionlessFile(path);
147+
}
148+
149+
// Toggleable overrides for package.json C++ methods (VFS support).
150+
let _loaderReadPackageJSON = null;
151+
let _loaderGetNearestParentPackageJSON = null;
152+
let _loaderGetPackageScopeConfig = null;
153+
let _loaderGetPackageType = null;
154+
155+
/**
156+
* Set override functions for the module loader's package.json operations.
157+
* @param {{
158+
* readPackageJSON?: Function,
159+
* getNearestParentPackageJSON?: Function,
160+
* getPackageScopeConfig?: Function,
161+
* getPackageType?: Function,
162+
* }} overrides
163+
*/
164+
function setLoaderPackageOverrides(overrides) {
165+
_loaderReadPackageJSON = overrides.readPackageJSON;
166+
_loaderGetNearestParentPackageJSON = overrides.getNearestParentPackageJSON;
167+
_loaderGetPackageScopeConfig = overrides.getPackageScopeConfig;
168+
_loaderGetPackageType = overrides.getPackageType;
169+
}
170+
171+
/**
172+
* Wrapper for modulesBinding.readPackageJSON that supports VFS toggle.
173+
* @param {string} jsonPath
174+
* @param {boolean} isESM
175+
* @param {string} base
176+
* @param {string} specifier
177+
* @returns {object|undefined}
178+
*/
179+
function loaderReadPackageJSON(jsonPath, isESM, base, specifier) {
180+
if (_loaderReadPackageJSON !== null) {
181+
return _loaderReadPackageJSON(jsonPath, isESM, base, specifier);
182+
}
183+
return modulesBinding.readPackageJSON(jsonPath, isESM, base, specifier);
184+
}
185+
186+
/**
187+
* Wrapper for modulesBinding.getNearestParentPackageJSON that supports VFS toggle.
188+
* @param {string} checkPath
189+
* @returns {object|undefined}
190+
*/
191+
function loaderGetNearestParentPackageJSON(checkPath) {
192+
if (_loaderGetNearestParentPackageJSON !== null) {
193+
return _loaderGetNearestParentPackageJSON(checkPath);
194+
}
195+
return modulesBinding.getNearestParentPackageJSON(checkPath);
196+
}
197+
198+
/**
199+
* Wrapper for modulesBinding.getPackageScopeConfig that supports VFS toggle.
200+
* @param {string} resolved
201+
* @returns {object|string}
202+
*/
203+
function loaderGetPackageScopeConfig(resolved) {
204+
if (_loaderGetPackageScopeConfig !== null) {
205+
return _loaderGetPackageScopeConfig(resolved);
206+
}
207+
return modulesBinding.getPackageScopeConfig(resolved);
208+
}
209+
210+
/**
211+
* Wrapper for modulesBinding.getPackageType that supports VFS toggle.
212+
* @param {string} url
213+
* @returns {string|undefined}
214+
*/
215+
function loaderGetPackageType(url) {
216+
if (_loaderGetPackageType !== null) {
217+
return _loaderGetPackageType(url);
218+
}
219+
return modulesBinding.getPackageType(url);
220+
}
221+
70222
/** @type {Set<string>} */
71223
let cjsConditions;
72224
/** @type {string[]} */
@@ -524,10 +676,20 @@ module.exports = {
524676
getCjsConditionsArray,
525677
getCompileCacheDir,
526678
initializeCjsConditions,
679+
loaderGetFormatOfExtensionlessFile,
680+
loaderGetNearestParentPackageJSON,
681+
loaderGetPackageScopeConfig,
682+
loaderGetPackageType,
683+
loaderLegacyMainResolve,
684+
loaderReadFile,
685+
loaderReadPackageJSON,
686+
loaderStat,
527687
loadBuiltinModuleForEmbedder,
528688
loadBuiltinModule,
529689
makeRequireFunction,
530690
normalizeReferrerURL,
691+
setLoaderFsOverrides,
692+
setLoaderPackageOverrides,
531693
stringify,
532694
stripBOM,
533695
toRealPath,

0 commit comments

Comments
 (0)