Skip to content

Commit a556bbf

Browse files
committed
vfs: add ESM loader intercepts for legacyMainResolve and getFormatOfExtensionlessFile
Both internalBinding('fs') methods bypass VFS fs-level overrides since they use uv_fs_stat/read directly in C++. Route them through toggleable wrappers in helpers.js so the VFS can handle paths under active mounts.
1 parent f0d7b40 commit a556bbf

File tree

5 files changed

+144
-8
lines changed

5 files changed

+144
-8
lines changed

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/resolve.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ const { sep, posix: { relative: relativePosixPath }, resolve } = require('path')
2929
const { URL, pathToFileURL, fileURLToPath, isURL, URLParse } = require('internal/url');
3030
const { getCWDURL, setOwnProperty } = require('internal/util');
3131
const { canParse: URLCanParse } = internalBinding('url');
32-
const { legacyMainResolve: FSLegacyMainResolve } = internalBinding('fs');
3332
const {
3433
ERR_INPUT_TYPE_NOT_ALLOWED,
3534
ERR_INVALID_ARG_TYPE,
@@ -46,7 +45,7 @@ const {
4645
const { defaultGetFormatWithoutErrors } = require('internal/modules/esm/get_format');
4746
const { getConditionsSet } = require('internal/modules/esm/utils');
4847
const packageJsonReader = require('internal/modules/package_json_reader');
49-
const { loaderStat, toRealPath } = require('internal/modules/helpers');
48+
const { loaderLegacyMainResolve, loaderStat, toRealPath } = require('internal/modules/helpers');
5049

5150
/**
5251
* @typedef {import('internal/modules/esm/package_config.js').PackageConfig} PackageConfig
@@ -193,7 +192,7 @@ function legacyMainResolve(packageJSONUrl, packageConfig, base) {
193192

194193
const baseStringified = isURL(base) ? base.href : base;
195194

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

198197
const maybeMain = resolvedOption <= legacyMainResolveExtensionsIndexes.kResolvedByMainIndexNode ?
199198
packageConfig.main || './' : '';

lib/internal/modules/helpers.js

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,20 @@ const realpathCache = new SafeMap();
6363
let _loaderStat = null;
6464
let _loaderReadFile = null;
6565
let _loaderRealpath = null;
66+
let _loaderLegacyMainResolve = null;
67+
let _loaderGetFormatOfExtensionlessFile = null;
6668

6769
/**
6870
* Set override functions for the module loader's fs operations.
69-
* @param {{ stat?: Function, readFile?: Function, realpath?: Function }} overrides
71+
* @param {{ stat?: Function, readFile?: Function, realpath?: Function,
72+
* legacyMainResolve?: Function, getFormatOfExtensionlessFile?: Function }} overrides
7073
*/
71-
function setLoaderFsOverrides({ stat, readFile, realpath }) {
74+
function setLoaderFsOverrides({ stat, readFile, realpath, legacyMainResolve, getFormatOfExtensionlessFile }) {
7275
_loaderStat = stat;
7376
_loaderReadFile = readFile;
7477
_loaderRealpath = realpath;
78+
_loaderLegacyMainResolve = legacyMainResolve;
79+
_loaderGetFormatOfExtensionlessFile = getFormatOfExtensionlessFile;
7580
}
7681

7782
/**
@@ -113,6 +118,34 @@ function toRealPath(requestPath) {
113118
});
114119
}
115120

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+
116149
// Toggleable overrides for package.json C++ methods (VFS support).
117150
let _loaderReadPackageJSON = null;
118151
let _loaderGetNearestParentPackageJSON = null;
@@ -643,9 +676,11 @@ module.exports = {
643676
getCjsConditionsArray,
644677
getCompileCacheDir,
645678
initializeCjsConditions,
679+
loaderGetFormatOfExtensionlessFile,
646680
loaderGetNearestParentPackageJSON,
647681
loaderGetPackageScopeConfig,
648682
loaderGetPackageType,
683+
loaderLegacyMainResolve,
649684
loaderReadFile,
650685
loaderReadPackageJSON,
651686
loaderStat,

lib/internal/vfs/setup.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,6 +1360,56 @@ function installModuleLoaderOverrides() {
13601360
const result = findVFSForRealpath(filename);
13611361
return result !== null ? result.realpath : undefined;
13621362
},
1363+
legacyMainResolve(pkgPath, main, base) {
1364+
// Check if pkgPath is under any active VFS
1365+
const normalized = resolve(pkgPath);
1366+
let handled = false;
1367+
for (let i = 0; i < activeVFSList.length; i++) {
1368+
if (activeVFSList[i].shouldHandle(normalized)) {
1369+
handled = true;
1370+
break;
1371+
}
1372+
}
1373+
if (!handled) return undefined;
1374+
1375+
// Extension index mapping (matches C++ legacyMainResolve):
1376+
// 0-6: try main + extension, then main + /index.ext
1377+
// 7-9: try pkgPath + ./index.ext
1378+
const mainExts = ['', '.js', '.json', '.node',
1379+
'/index.js', '/index.json', '/index.node'];
1380+
const indexExts = ['./index.js', './index.json', './index.node'];
1381+
1382+
if (main) {
1383+
for (let i = 0; i < mainExts.length; i++) {
1384+
const candidate = resolve(pkgPath, main + mainExts[i]);
1385+
if (findVFSForStat(candidate)?.result === 0) return i;
1386+
}
1387+
}
1388+
for (let i = 0; i < indexExts.length; i++) {
1389+
const candidate = resolve(pkgPath, indexExts[i]);
1390+
if (findVFSForStat(candidate)?.result === 0) return 7 + i;
1391+
}
1392+
1393+
const { ERR_MODULE_NOT_FOUND } = require('internal/errors').codes;
1394+
throw new ERR_MODULE_NOT_FOUND(
1395+
pkgPath, base, 'package');
1396+
},
1397+
getFormatOfExtensionlessFile(filePath) {
1398+
try {
1399+
const result = findVFSForRead(filePath, null);
1400+
if (result === null) return undefined;
1401+
const content = result.content;
1402+
// Check for Wasm magic bytes: 0x00 0x61 0x73 0x6d
1403+
if (content && content.length >= 4 &&
1404+
content[0] === 0x00 && content[1] === 0x61 &&
1405+
content[2] === 0x73 && content[3] === 0x6d) {
1406+
return 1; // EXTENSIONLESS_FORMAT_WASM
1407+
}
1408+
return 0; // EXTENSIONLESS_FORMAT_JAVASCRIPT
1409+
} catch {
1410+
return 0; // EXTENSIONLESS_FORMAT_JAVASCRIPT
1411+
}
1412+
},
13631413
});
13641414
const nativeModulesBinding = internalBinding('modules');
13651415
setLoaderPackageOverrides({

test/parallel/test-vfs-require.js

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict';
22

3-
require('../common');
3+
const common = require('../common');
44
const assert = require('assert');
55
const fs = require('fs');
66
const path = require('path');
@@ -291,6 +291,58 @@ const vfs = require('node:vfs');
291291
myVfs.unmount();
292292
}
293293

294+
// Test legacyMainResolve: package with "main" field resolves through VFS
295+
{
296+
const myVfs = vfs.create();
297+
myVfs.mkdirSync('/pkg/lib', { recursive: true });
298+
myVfs.writeFileSync('/pkg/package.json', JSON.stringify({
299+
name: 'legacy-main-pkg',
300+
main: './lib/entry',
301+
}));
302+
myVfs.writeFileSync('/pkg/lib/entry.js', 'module.exports = "legacy-main";');
303+
myVfs.mount('/virtual20');
304+
305+
const result = require('/virtual20/pkg');
306+
assert.strictEqual(result, 'legacy-main');
307+
308+
myVfs.unmount();
309+
}
310+
311+
// Test legacyMainResolve: package with no "main" field resolves index.js
312+
{
313+
const myVfs = vfs.create();
314+
myVfs.mkdirSync('/pkg2', { recursive: true });
315+
myVfs.writeFileSync('/pkg2/package.json', JSON.stringify({
316+
name: 'no-main-pkg',
317+
}));
318+
myVfs.writeFileSync('/pkg2/index.js', 'module.exports = "index-fallback";');
319+
myVfs.mount('/virtual21');
320+
321+
const result = require('/virtual21/pkg2');
322+
assert.strictEqual(result, 'index-fallback');
323+
324+
myVfs.unmount();
325+
}
326+
327+
// Test getFormatOfExtensionlessFile: extensionless JS file in type:module package
328+
{
329+
const myVfs = vfs.create();
330+
myVfs.mkdirSync('/esm-pkg', { recursive: true });
331+
myVfs.writeFileSync('/esm-pkg/package.json', JSON.stringify({
332+
name: 'esm-pkg',
333+
type: 'module',
334+
}));
335+
myVfs.writeFileSync('/esm-pkg/entry', 'export const x = 123;');
336+
myVfs.mount('/virtual22');
337+
338+
// Use import() to trigger ESM loader path
339+
const importPromise = import('/virtual22/esm-pkg/entry');
340+
importPromise.then(common.mustCall((mod) => {
341+
assert.strictEqual(mod.x, 123);
342+
myVfs.unmount();
343+
}));
344+
}
345+
294346
// Test that unmounting stops interception
295347
{
296348
const myVfs = vfs.create();

0 commit comments

Comments
 (0)