Skip to content

Commit 3b8e89c

Browse files
committed
vfs: add VFS-aware wrappers for package.json reader methods
Wrap modulesBinding.readPackageJSON(), getNearestParentPackageJSON(), getPackageScopeConfig(), and getPackageType() with toggleable overrides so that VFS-mounted package.json files are read from virtual storage instead of the real filesystem. This fixes syntax detection and error decoration that re-read package.json bypassing VFS. The implementation follows the existing loaderStat/loaderReadFile toggle pattern in helpers.js.
1 parent 2de47ca commit 3b8e89c

File tree

4 files changed

+421
-8
lines changed

4 files changed

+421
-8
lines changed

lib/internal/modules/helpers.js

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,13 @@ const { join } = path;
3636

3737
const internalFsBinding = internalBinding('fs');
3838
const { canParse: URLCanParse } = internalBinding('url');
39+
const modulesBinding = internalBinding('modules');
3940
const {
4041
enableCompileCache: _enableCompileCache,
4142
getCompileCacheDir: _getCompileCacheDir,
4243
compileCacheStatus: _compileCacheStatus,
4344
flushCompileCache,
44-
} = internalBinding('modules');
45+
} = modulesBinding;
4546

4647
const lazyCJSLoader = getLazy(() => require('internal/modules/cjs/loader'));
4748
let debug = require('internal/util/debuglog').debuglog('module', (fn) => {
@@ -112,6 +113,79 @@ function toRealPath(requestPath) {
112113
});
113114
}
114115

116+
// Toggleable overrides for package.json C++ methods (VFS support).
117+
let _loaderReadPackageJSON = null;
118+
let _loaderGetNearestParentPackageJSON = null;
119+
let _loaderGetPackageScopeConfig = null;
120+
let _loaderGetPackageType = null;
121+
122+
/**
123+
* Set override functions for the module loader's package.json operations.
124+
* @param {{
125+
* readPackageJSON?: Function,
126+
* getNearestParentPackageJSON?: Function,
127+
* getPackageScopeConfig?: Function,
128+
* getPackageType?: Function,
129+
* }} overrides
130+
*/
131+
function setLoaderPackageOverrides(overrides) {
132+
_loaderReadPackageJSON = overrides.readPackageJSON;
133+
_loaderGetNearestParentPackageJSON = overrides.getNearestParentPackageJSON;
134+
_loaderGetPackageScopeConfig = overrides.getPackageScopeConfig;
135+
_loaderGetPackageType = overrides.getPackageType;
136+
}
137+
138+
/**
139+
* Wrapper for modulesBinding.readPackageJSON that supports VFS toggle.
140+
* @param {string} jsonPath
141+
* @param {boolean} isESM
142+
* @param {string} base
143+
* @param {string} specifier
144+
* @returns {SerializedPackageConfig|undefined}
145+
*/
146+
function loaderReadPackageJSON(jsonPath, isESM, base, specifier) {
147+
if (_loaderReadPackageJSON !== null) {
148+
return _loaderReadPackageJSON(jsonPath, isESM, base, specifier);
149+
}
150+
return modulesBinding.readPackageJSON(jsonPath, isESM, base, specifier);
151+
}
152+
153+
/**
154+
* Wrapper for modulesBinding.getNearestParentPackageJSON that supports VFS toggle.
155+
* @param {string} checkPath
156+
* @returns {SerializedPackageConfig|undefined}
157+
*/
158+
function loaderGetNearestParentPackageJSON(checkPath) {
159+
if (_loaderGetNearestParentPackageJSON !== null) {
160+
return _loaderGetNearestParentPackageJSON(checkPath);
161+
}
162+
return modulesBinding.getNearestParentPackageJSON(checkPath);
163+
}
164+
165+
/**
166+
* Wrapper for modulesBinding.getPackageScopeConfig that supports VFS toggle.
167+
* @param {string} resolved
168+
* @returns {SerializedPackageConfig|string}
169+
*/
170+
function loaderGetPackageScopeConfig(resolved) {
171+
if (_loaderGetPackageScopeConfig !== null) {
172+
return _loaderGetPackageScopeConfig(resolved);
173+
}
174+
return modulesBinding.getPackageScopeConfig(resolved);
175+
}
176+
177+
/**
178+
* Wrapper for modulesBinding.getPackageType that supports VFS toggle.
179+
* @param {string} url
180+
* @returns {string|undefined}
181+
*/
182+
function loaderGetPackageType(url) {
183+
if (_loaderGetPackageType !== null) {
184+
return _loaderGetPackageType(url);
185+
}
186+
return modulesBinding.getPackageType(url);
187+
}
188+
115189
/** @type {Set<string>} */
116190
let cjsConditions;
117191
/** @type {string[]} */
@@ -569,13 +643,18 @@ module.exports = {
569643
getCjsConditionsArray,
570644
getCompileCacheDir,
571645
initializeCjsConditions,
646+
loaderGetNearestParentPackageJSON,
647+
loaderGetPackageScopeConfig,
648+
loaderGetPackageType,
572649
loaderReadFile,
650+
loaderReadPackageJSON,
573651
loaderStat,
574652
loadBuiltinModuleForEmbedder,
575653
loadBuiltinModule,
576654
makeRequireFunction,
577655
normalizeReferrerURL,
578656
setLoaderFsOverrides,
657+
setLoaderPackageOverrides,
579658
stringify,
580659
stripBOM,
581660
toRealPath,

lib/internal/modules/package_json_reader.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,15 @@ const {
2424
},
2525
} = require('internal/errors');
2626
const { kEmptyObject } = require('internal/util');
27-
const modulesBinding = internalBinding('modules');
2827
const path = require('path');
2928
const { validateString } = require('internal/validators');
30-
const { loaderStat } = require('internal/modules/helpers');
29+
const {
30+
loaderGetNearestParentPackageJSON,
31+
loaderGetPackageScopeConfig,
32+
loaderGetPackageType,
33+
loaderReadPackageJSON,
34+
loaderStat,
35+
} = require('internal/modules/helpers');
3136

3237

3338
/**
@@ -122,7 +127,7 @@ const requiresJSONParse = (value) => (value !== undefined && (value[0] === '[' |
122127
function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
123128
// This function will be called by both CJS and ESM, so we need to make sure
124129
// non-null attributes are converted to strings.
125-
const parsed = modulesBinding.readPackageJSON(
130+
const parsed = loaderReadPackageJSON(
126131
jsonPath,
127132
isESM,
128133
base == null ? undefined : `${base}`,
@@ -170,7 +175,7 @@ function getNearestParentPackageJSON(checkPath) {
170175
return deserializedPackageJSONCache.get(parentPackageJSONPath);
171176
}
172177

173-
const result = modulesBinding.getNearestParentPackageJSON(checkPath);
178+
const result = loaderGetNearestParentPackageJSON(checkPath);
174179
const packageConfig = deserializePackageJSON(checkPath, result);
175180

176181
moduleToParentPackageJSONCache.set(checkPath, packageConfig.path);
@@ -190,7 +195,7 @@ function getNearestParentPackageJSON(checkPath) {
190195
* @returns {import('typings/internalBinding/modules').PackageConfig} - The package configuration.
191196
*/
192197
function getPackageScopeConfig(resolved) {
193-
const result = modulesBinding.getPackageScopeConfig(`${resolved}`);
198+
const result = loaderGetPackageScopeConfig(`${resolved}`);
194199

195200
if (ArrayIsArray(result)) {
196201
const { data, exists, path } = deserializePackageJSON(`${resolved}`, result);
@@ -219,7 +224,7 @@ function getPackageScopeConfig(resolved) {
219224
* @returns {string}
220225
*/
221226
function getPackageType(url) {
222-
const type = modulesBinding.getPackageType(`${url}`);
227+
const type = loaderGetPackageType(`${url}`);
223228
return type ?? 'none';
224229
}
225230

lib/internal/vfs/module_hooks.js

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const {
77
ArrayPrototypeSplice,
88
FunctionPrototypeCall,
99
JSONParse,
10+
JSONStringify,
1011
ObjectGetOwnPropertyNames,
1112
String,
1213
StringPrototypeEndsWith,
@@ -870,6 +871,60 @@ function vfsLoadHook(url, context, nextLoad) {
870871
return nextLoad(url, context);
871872
}
872873

874+
/**
875+
* Serialize a parsed package.json object into the C++ tuple format
876+
* expected by deserializePackageJSON: [name, main, type, imports, exports, filePath].
877+
* @param {object} parsed The parsed package.json content
878+
* @param {string} filePath The path to the package.json file
879+
* @returns {Array} Serialized package config tuple
880+
*/
881+
function serializePackageJSON(parsed, filePath) {
882+
const name = parsed.name;
883+
const main = parsed.main;
884+
const type = parsed.type ?? 'none';
885+
const imports = parsed.imports !== undefined ?
886+
(typeof parsed.imports === 'string' ?
887+
parsed.imports : JSONStringify(parsed.imports)) :
888+
undefined;
889+
const exports = parsed.exports !== undefined ?
890+
(typeof parsed.exports === 'string' ?
891+
parsed.exports : JSONStringify(parsed.exports)) :
892+
undefined;
893+
return [name, main, type, imports, exports, filePath];
894+
}
895+
896+
/**
897+
* Walk up directories in VFS looking for package.json.
898+
* @param {string} startPath Normalized absolute path to start from
899+
* @returns {{ vfs: VirtualFileSystem, pjsonPath: string, parsed: object }|null}
900+
*/
901+
function findVFSPackageJSON(startPath) {
902+
let currentDir = dirname(startPath);
903+
let lastDir;
904+
while (currentDir !== lastDir) {
905+
if (StringPrototypeEndsWith(currentDir, '/node_modules') ||
906+
StringPrototypeEndsWith(currentDir, '\\node_modules')) {
907+
break;
908+
}
909+
const pjsonPath = resolve(currentDir, 'package.json');
910+
for (let i = 0; i < activeVFSList.length; i++) {
911+
const vfs = activeVFSList[i];
912+
if (vfs.shouldHandle(pjsonPath) && vfsStat(vfs, pjsonPath) === 0) {
913+
try {
914+
const content = vfs.readFileSync(pjsonPath, 'utf8');
915+
const parsed = JSONParse(content);
916+
return { vfs, pjsonPath, parsed };
917+
} catch {
918+
// Invalid JSON, continue walking
919+
}
920+
}
921+
}
922+
lastDir = currentDir;
923+
currentDir = dirname(currentDir);
924+
}
925+
return null;
926+
}
927+
873928
/**
874929
* Install module loading hooks via Module.registerHooks.
875930
* Both CJS and ESM go through the hooks chain. When both hooks
@@ -1086,7 +1141,10 @@ function installFsPatches() {
10861141
* references to the original fs methods.
10871142
*/
10881143
function installModuleLoaderOverrides() {
1089-
const { setLoaderFsOverrides } = require('internal/modules/helpers');
1144+
const {
1145+
setLoaderFsOverrides,
1146+
setLoaderPackageOverrides,
1147+
} = require('internal/modules/helpers');
10901148
setLoaderFsOverrides({
10911149
stat(filename) {
10921150
const result = findVFSForStat(filename);
@@ -1104,6 +1162,92 @@ function installModuleLoaderOverrides() {
11041162
return result !== null ? result.realpath : undefined;
11051163
},
11061164
});
1165+
const nativeModulesBinding = internalBinding('modules');
1166+
setLoaderPackageOverrides({
1167+
readPackageJSON(jsonPath, isESM, base, specifier) {
1168+
const normalized = resolve(jsonPath);
1169+
for (let i = 0; i < activeVFSList.length; i++) {
1170+
const vfs = activeVFSList[i];
1171+
if (!vfs.shouldHandle(normalized)) continue;
1172+
if (vfsStat(vfs, normalized) !== 0) return undefined;
1173+
try {
1174+
const content = vfs.readFileSync(normalized, 'utf8');
1175+
const parsed = JSONParse(content);
1176+
return serializePackageJSON(parsed, normalized);
1177+
} catch {
1178+
return undefined;
1179+
}
1180+
}
1181+
return nativeModulesBinding.readPackageJSON(
1182+
jsonPath, isESM, base, specifier);
1183+
},
1184+
getNearestParentPackageJSON(checkPath) {
1185+
const normalized = resolve(checkPath);
1186+
// Check if this path is within any VFS
1187+
for (let i = 0; i < activeVFSList.length; i++) {
1188+
if (activeVFSList[i].shouldHandle(normalized)) {
1189+
const found = findVFSPackageJSON(normalized);
1190+
if (found !== null) {
1191+
return serializePackageJSON(found.parsed, found.pjsonPath);
1192+
}
1193+
// Path is in VFS but no package.json found
1194+
return undefined;
1195+
}
1196+
}
1197+
return nativeModulesBinding.getNearestParentPackageJSON(checkPath);
1198+
},
1199+
getPackageScopeConfig(resolved) {
1200+
// Resolved is a URL string like 'file:///path/to/file.js'
1201+
let filePath;
1202+
if (StringPrototypeStartsWith(resolved, 'file:')) {
1203+
try {
1204+
filePath = fileURLToPath(resolved);
1205+
} catch {
1206+
return nativeModulesBinding.getPackageScopeConfig(resolved);
1207+
}
1208+
} else {
1209+
filePath = resolved;
1210+
}
1211+
const normalized = resolve(filePath);
1212+
for (let i = 0; i < activeVFSList.length; i++) {
1213+
if (activeVFSList[i].shouldHandle(normalized)) {
1214+
const found = findVFSPackageJSON(normalized);
1215+
if (found !== null) {
1216+
return serializePackageJSON(found.parsed, found.pjsonPath);
1217+
}
1218+
// Not found in VFS - return string path (matching C++ behavior)
1219+
return resolve(dirname(normalized), 'package.json');
1220+
}
1221+
}
1222+
return nativeModulesBinding.getPackageScopeConfig(resolved);
1223+
},
1224+
getPackageType(url) {
1225+
let filePath;
1226+
if (StringPrototypeStartsWith(url, 'file:')) {
1227+
try {
1228+
filePath = fileURLToPath(url);
1229+
} catch {
1230+
return nativeModulesBinding.getPackageType(url);
1231+
}
1232+
} else {
1233+
filePath = url;
1234+
}
1235+
const normalized = resolve(filePath);
1236+
for (let i = 0; i < activeVFSList.length; i++) {
1237+
if (activeVFSList[i].shouldHandle(normalized)) {
1238+
const found = findVFSPackageJSON(normalized);
1239+
if (found !== null) {
1240+
const type = found.parsed.type;
1241+
if (type === 'module' || type === 'commonjs') {
1242+
return type;
1243+
}
1244+
}
1245+
return undefined;
1246+
}
1247+
}
1248+
return nativeModulesBinding.getPackageType(url);
1249+
},
1250+
});
11071251
}
11081252

11091253
/**

0 commit comments

Comments
 (0)