Skip to content

Commit 7806a59

Browse files
committed
lib: merge cjs and esm package json reader caches
1 parent 0d725d6 commit 7806a59

File tree

7 files changed

+119
-216
lines changed

7 files changed

+119
-216
lines changed

lib/internal/modules/cjs/loader.js

+14-38
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ const {
8282
pendingDeprecate,
8383
emitExperimentalWarning,
8484
kEmptyObject,
85-
filterOwnProperties,
8685
setOwnProperty,
8786
getLazy,
8887
} = require('internal/util');
@@ -353,36 +352,12 @@ function initializeCJS() {
353352
// -> a.<ext>
354353
// -> a/index.<ext>
355354

356-
const packageJsonCache = new SafeMap();
357-
355+
/**
356+
* @param {string} requestPath
357+
* @return {PackageConfig}
358+
*/
358359
function readPackage(requestPath) {
359-
const jsonPath = path.resolve(requestPath, 'package.json');
360-
361-
const existing = packageJsonCache.get(jsonPath);
362-
if (existing !== undefined) return existing;
363-
364-
const result = packageJsonReader.read(jsonPath);
365-
const json = result.containsKeys === false ? '{}' : result.string;
366-
if (json === undefined) {
367-
packageJsonCache.set(jsonPath, false);
368-
return false;
369-
}
370-
371-
try {
372-
const filtered = filterOwnProperties(JSONParse(json), [
373-
'name',
374-
'main',
375-
'exports',
376-
'imports',
377-
'type',
378-
]);
379-
packageJsonCache.set(jsonPath, filtered);
380-
return filtered;
381-
} catch (e) {
382-
e.path = jsonPath;
383-
e.message = 'Error parsing ' + jsonPath + ': ' + e.message;
384-
throw e;
385-
}
360+
return packageJsonReader.read(path.resolve(requestPath, 'package.json'));
386361
}
387362

388363
let _readPackage = readPackage;
@@ -412,7 +387,7 @@ function readPackageScope(checkPath) {
412387
if (StringPrototypeEndsWith(checkPath, sep + 'node_modules'))
413388
return false;
414389
const pjson = _readPackage(checkPath + sep);
415-
if (pjson) return {
390+
if (pjson.exists) return {
416391
data: pjson,
417392
path: checkPath,
418393
};
@@ -421,7 +396,7 @@ function readPackageScope(checkPath) {
421396
}
422397

423398
function tryPackage(requestPath, exts, isMain, originalPath) {
424-
const pkg = _readPackage(requestPath)?.main;
399+
const pkg = _readPackage(requestPath).main;
425400

426401
if (!pkg) {
427402
return tryExtensions(path.resolve(requestPath, 'index'), exts, isMain);
@@ -526,9 +501,10 @@ function trySelfParentPath(parent) {
526501
function trySelf(parentPath, request) {
527502
if (!parentPath) return false;
528503

529-
const { data: pkg, path: pkgPath } = readPackageScope(parentPath) || {};
530-
if (!pkg || pkg.exports === undefined) return false;
531-
if (typeof pkg.name !== 'string') return false;
504+
const { data: pkg, path: pkgPath } = readPackageScope(parentPath);
505+
if (!pkg || pkg.exports == null || pkg.name === undefined) {
506+
return false;
507+
}
532508

533509
let expansion;
534510
if (request === pkg.name) {
@@ -563,7 +539,7 @@ function resolveExports(nmPath, request) {
563539
return;
564540
const pkgPath = path.resolve(nmPath, name);
565541
const pkg = _readPackage(pkgPath);
566-
if (pkg?.exports != null) {
542+
if (pkg.exists && pkg.exports != null) {
567543
try {
568544
const { packageExportsResolve } = require('internal/modules/esm/resolve');
569545
return finalizeEsmResolution(packageExportsResolve(
@@ -1272,9 +1248,9 @@ Module._extensions['.js'] = function(module, filename) {
12721248
content = fs.readFileSync(filename, 'utf8');
12731249
}
12741250
if (StringPrototypeEndsWith(filename, '.js')) {
1275-
const pkg = readPackageScope(filename);
1251+
const pkg = readPackageScope(filename) || {};
12761252
// Function require shouldn't be used in ES modules.
1277-
if (pkg?.data?.type === 'module') {
1253+
if (pkg.data?.type === 'module') {
12781254
const parent = moduleParentCache.get(module);
12791255
const parentPath = parent?.filename;
12801256
const packageJsonPath = path.resolve(pkg.path, 'package.json');
+3-98
Original file line numberDiff line numberDiff line change
@@ -1,102 +1,10 @@
11
'use strict';
22

33
const {
4-
JSONParse,
5-
ObjectPrototypeHasOwnProperty,
6-
SafeMap,
74
StringPrototypeEndsWith,
85
} = primordials;
96
const { URL, fileURLToPath } = require('internal/url');
10-
const {
11-
ERR_INVALID_PACKAGE_CONFIG,
12-
} = require('internal/errors').codes;
13-
14-
const { filterOwnProperties } = require('internal/util');
15-
16-
17-
/**
18-
* @typedef {string | string[] | Record<string, unknown>} Exports
19-
* @typedef {'module' | 'commonjs'} PackageType
20-
* @typedef {{
21-
* pjsonPath: string,
22-
* exports?: ExportConfig,
23-
* name?: string,
24-
* main?: string,
25-
* type?: PackageType,
26-
* }} PackageConfig
27-
*/
28-
29-
/** @type {Map<string, PackageConfig>} */
30-
const packageJSONCache = new SafeMap();
31-
32-
33-
/**
34-
* @param {string} path
35-
* @param {string} specifier
36-
* @param {string | URL | undefined} base
37-
* @returns {PackageConfig}
38-
*/
39-
function getPackageConfig(path, specifier, base) {
40-
const existing = packageJSONCache.get(path);
41-
if (existing !== undefined) {
42-
return existing;
43-
}
44-
const packageJsonReader = require('internal/modules/package_json_reader');
45-
const source = packageJsonReader.read(path).string;
46-
if (source === undefined) {
47-
const packageConfig = {
48-
pjsonPath: path,
49-
exists: false,
50-
main: undefined,
51-
name: undefined,
52-
type: 'none',
53-
exports: undefined,
54-
imports: undefined,
55-
};
56-
packageJSONCache.set(path, packageConfig);
57-
return packageConfig;
58-
}
59-
60-
let packageJSON;
61-
try {
62-
packageJSON = JSONParse(source);
63-
} catch (error) {
64-
throw new ERR_INVALID_PACKAGE_CONFIG(
65-
path,
66-
(base ? `"${specifier}" from ` : '') + fileURLToPath(base || specifier),
67-
error.message,
68-
);
69-
}
70-
71-
let { imports, main, name, type } = filterOwnProperties(packageJSON, ['imports', 'main', 'name', 'type']);
72-
const exports = ObjectPrototypeHasOwnProperty(packageJSON, 'exports') ? packageJSON.exports : undefined;
73-
if (typeof imports !== 'object' || imports === null) {
74-
imports = undefined;
75-
}
76-
if (typeof main !== 'string') {
77-
main = undefined;
78-
}
79-
if (typeof name !== 'string') {
80-
name = undefined;
81-
}
82-
// Ignore unknown types for forwards compatibility
83-
if (type !== 'module' && type !== 'commonjs') {
84-
type = 'none';
85-
}
86-
87-
const packageConfig = {
88-
pjsonPath: path,
89-
exists: true,
90-
main,
91-
name,
92-
type,
93-
exports,
94-
imports,
95-
};
96-
packageJSONCache.set(path, packageConfig);
97-
return packageConfig;
98-
}
99-
7+
const packageJsonReader = require('internal/modules/package_json_reader');
1008

1019
/**
10210
* @param {URL | string} resolved
@@ -109,7 +17,7 @@ function getPackageScopeConfig(resolved) {
10917
if (StringPrototypeEndsWith(packageJSONPath, 'node_modules/package.json')) {
11018
break;
11119
}
112-
const packageConfig = getPackageConfig(fileURLToPath(packageJSONUrl), resolved);
20+
const packageConfig = packageJsonReader.read(fileURLToPath(packageJSONUrl), { specifier: resolved, isESM: true });
11321
if (packageConfig.exists) {
11422
return packageConfig;
11523
}
@@ -124,7 +32,7 @@ function getPackageScopeConfig(resolved) {
12432
}
12533
}
12634
const packageJSONPath = fileURLToPath(packageJSONUrl);
127-
const packageConfig = {
35+
return {
12836
pjsonPath: packageJSONPath,
12937
exists: false,
13038
main: undefined,
@@ -133,12 +41,9 @@ function getPackageScopeConfig(resolved) {
13341
exports: undefined,
13442
imports: undefined,
13543
};
136-
packageJSONCache.set(packageJSONPath, packageConfig);
137-
return packageConfig;
13844
}
13945

14046

14147
module.exports = {
142-
getPackageConfig,
14348
getPackageScopeConfig,
14449
};

lib/internal/modules/esm/resolve.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,9 @@ const {
5353
} = require('internal/errors').codes;
5454

5555
const { Module: CJSModule } = require('internal/modules/cjs/loader');
56-
const { getPackageConfig, getPackageScopeConfig } = require('internal/modules/esm/package_config');
56+
const { getPackageScopeConfig } = require('internal/modules/esm/package_config');
5757
const { getConditionsSet } = require('internal/modules/esm/utils');
58+
const packageJsonReader = require('internal/modules/package_json_reader');
5859
const { internalModuleStat } = internalBinding('fs');
5960

6061
/**
@@ -734,8 +735,7 @@ function packageResolve(specifier, base, conditions) {
734735
const packageConfig = getPackageScopeConfig(base);
735736
if (packageConfig.exists) {
736737
const packageJSONUrl = pathToFileURL(packageConfig.pjsonPath);
737-
if (packageConfig.name === packageName &&
738-
packageConfig.exports !== undefined && packageConfig.exports !== null) {
738+
if (packageConfig.exports != null && packageConfig.name === packageName) {
739739
return packageExportsResolve(
740740
packageJSONUrl, packageSubpath, packageConfig, base, conditions);
741741
}
@@ -759,8 +759,8 @@ function packageResolve(specifier, base, conditions) {
759759
}
760760

761761
// Package match.
762-
const packageConfig = getPackageConfig(packageJSONPath, specifier, base);
763-
if (packageConfig.exports !== undefined && packageConfig.exports !== null) {
762+
const packageConfig = packageJsonReader.read(packageJSONPath, { specifier, base, isESM: true });
763+
if (packageConfig.exports != null) {
764764
return packageExportsResolve(
765765
packageJSONUrl, packageSubpath, packageConfig, base, conditions);
766766
}

lib/internal/modules/package_json_reader.js

+83-7
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,106 @@
11
'use strict';
22

3-
const { SafeMap } = primordials;
3+
const {
4+
JSONParse,
5+
ObjectPrototypeHasOwnProperty,
6+
SafeMap,
7+
} = primordials;
8+
const {
9+
ERR_INVALID_PACKAGE_CONFIG,
10+
} = require('internal/errors').codes;
411
const { internalModuleReadJSON } = internalBinding('fs');
5-
const { pathToFileURL } = require('url');
612
const { toNamespacedPath } = require('path');
13+
const { kEmptyObject } = require('internal/util');
14+
const { fileURLToPath, pathToFileURL } = require('internal/url');
715

816
const cache = new SafeMap();
917

1018
let manifest;
1119

1220
/**
13-
*
21+
* @typedef {{
22+
* exists: boolean,
23+
* pjsonPath: string,
24+
* exports?: string | string[] | Record<string, unknown>,
25+
* imports?: string | string[] | Record<string, unknown>,
26+
* name?: string,
27+
* main?: string,
28+
* type: 'commonjs' | 'module' | 'none',
29+
* }} PackageConfig
30+
*/
31+
32+
/**
1433
* @param {string} jsonPath
34+
* @param {{
35+
* base?: string,
36+
* specifier: string,
37+
* isESM: boolean,
38+
* }} options
39+
* @returns {PackageConfig}
1540
*/
16-
function read(jsonPath) {
41+
function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
1742
if (cache.has(jsonPath)) {
1843
return cache.get(jsonPath);
1944
}
2045

21-
const { 0: string, 1: containsKeys } = internalModuleReadJSON(
46+
const string = internalModuleReadJSON(
2247
toNamespacedPath(jsonPath),
2348
);
24-
const result = { string, containsKeys };
25-
const { getOptionValue } = require('internal/options');
49+
const result = {
50+
__proto__: null,
51+
exists: false,
52+
pjsonPath: jsonPath,
53+
main: undefined,
54+
name: undefined,
55+
type: 'none', // Ignore unknown types for forwards compatibility
56+
exports: undefined,
57+
imports: undefined,
58+
};
59+
2660
if (string !== undefined) {
61+
let parsed;
62+
try {
63+
parsed = JSONParse(string);
64+
} catch (error) {
65+
if (isESM) {
66+
throw new ERR_INVALID_PACKAGE_CONFIG(
67+
jsonPath,
68+
(base ? `"${specifier}" from ` : '') + fileURLToPath(base || specifier),
69+
error.message,
70+
);
71+
} else {
72+
error.message = 'Error parsing ' + jsonPath + ': ' + error.message;
73+
error.path = jsonPath;
74+
throw error;
75+
}
76+
}
77+
78+
result.exists = true;
79+
80+
// ObjectPrototypeHasOwnProperty is used to avoid prototype pollution.
81+
if (ObjectPrototypeHasOwnProperty(parsed, 'name') && typeof parsed.name === 'string') {
82+
result.name = parsed.name;
83+
}
84+
85+
if (ObjectPrototypeHasOwnProperty(parsed, 'main') && typeof parsed.main === 'string') {
86+
result.main = parsed.main;
87+
}
88+
89+
if (ObjectPrototypeHasOwnProperty(parsed, 'exports')) {
90+
result.exports = parsed.exports;
91+
}
92+
93+
if (ObjectPrototypeHasOwnProperty(parsed, 'imports')) {
94+
result.imports = parsed.imports;
95+
}
96+
97+
// Ignore unknown types for forwards compatibility
98+
if (ObjectPrototypeHasOwnProperty(parsed, 'type') && (parsed.type === 'commonjs' || parsed.type === 'module')) {
99+
result.type = parsed.type;
100+
}
101+
27102
if (manifest === undefined) {
103+
const { getOptionValue } = require('internal/options');
28104
manifest = getOptionValue('--experimental-policy') ?
29105
require('internal/process/policy').manifest :
30106
null;

0 commit comments

Comments
 (0)