Skip to content

Commit 3d0ac05

Browse files
committed
esm: share package.json string cache between ESM and CJS loaders
Refs: nodejs#30674
1 parent 1d4a52c commit 3d0ac05

File tree

7 files changed

+164
-108
lines changed

7 files changed

+164
-108
lines changed

lib/internal/modules/cjs/loader.js

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,8 @@ const fs = require('fs');
5959
const internalFS = require('internal/fs/utils');
6060
const path = require('path');
6161
const { emitWarningSync } = require('internal/process/warning');
62-
const {
63-
internalModuleReadJSON,
64-
internalModuleStat
65-
} = internalBinding('fs');
62+
const { internalModuleStat } = internalBinding('fs');
63+
const packageJsonReader = require('internal/modules/package_json_reader');
6664
const { safeGetenv } = internalBinding('credentials');
6765
const {
6866
makeRequireFunction,
@@ -94,10 +92,8 @@ const {
9492
const { validateString } = require('internal/validators');
9593
const pendingDeprecation = getOptionValue('--pending-deprecation');
9694

97-
const packageJsonCache = new SafeMap();
98-
9995
module.exports = {
100-
wrapSafe, Module, toRealPath, readPackageScope, readPackage, packageJsonCache,
96+
wrapSafe, Module, toRealPath, readPackageScope,
10197
get hasLoadedAnyUserCJSModule() { return hasLoadedAnyUserCJSModule; }
10298
};
10399

@@ -241,22 +237,17 @@ Module._debug = deprecate(debug, 'Module._debug is deprecated.', 'DEP0077');
241237
// -> a
242238
// -> a.<ext>
243239
// -> a/index.<ext>
244-
/**
245-
*
246-
* @param {string} jsonPath
247-
* @returns {string|undefined}
248-
*/
249-
function readJsonDefaultStrategy(jsonPath) {
250-
return internalModuleReadJSON(path.toNamespacedPath(jsonPath));
251-
}
252240

253-
function readPackage(requestPath, readJsonStrategy = readJsonDefaultStrategy) {
241+
const packageJsonCache = new SafeMap();
242+
243+
function readPackage(requestPath) {
254244
const jsonPath = path.resolve(requestPath, 'package.json');
255245

256246
const existing = packageJsonCache.get(jsonPath);
257247
if (existing !== undefined) return existing;
258248

259-
const json = readJsonStrategy(jsonPath);
249+
const result = packageJsonReader.read(path.toNamespacedPath(jsonPath));
250+
const json = result.containsKeys === false ? '{}' : result.string;
260251
if (json === undefined) {
261252
packageJsonCache.set(jsonPath, false);
262253
return false;
@@ -284,8 +275,7 @@ function readPackage(requestPath, readJsonStrategy = readJsonDefaultStrategy) {
284275
}
285276
}
286277

287-
function readPackageScope(
288-
checkPath, readJsonStrategy = readJsonDefaultStrategy) {
278+
function readPackageScope(checkPath) {
289279
const rootSeparatorIndex = checkPath.indexOf(path.sep);
290280
let separatorIndex;
291281
while (
@@ -294,7 +284,7 @@ function readPackageScope(
294284
checkPath = checkPath.slice(0, separatorIndex);
295285
if (checkPath.endsWith(path.sep + 'node_modules'))
296286
return false;
297-
const pjson = readPackage(checkPath, readJsonStrategy);
287+
const pjson = readPackage(checkPath);
298288
if (pjson) return {
299289
path: checkPath,
300290
data: pjson

lib/internal/modules/esm/resolve.js

Lines changed: 84 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const {
44
ArrayIsArray,
55
ArrayPrototypeJoin,
66
ArrayPrototypeShift,
7+
JSONParse,
78
JSONStringify,
89
ObjectFreeze,
910
ObjectGetOwnPropertyNames,
@@ -23,23 +24,14 @@ const {
2324
const assert = require('internal/assert');
2425
const internalFS = require('internal/fs/utils');
2526
const { NativeModule } = require('internal/bootstrap/loaders');
26-
const {
27-
Module: CJSModule,
28-
readPackageScope,
29-
readPackage,
30-
packageJsonCache
31-
} = require('internal/modules/cjs/loader');
3227
const {
3328
realpathSync,
3429
statSync,
3530
Stats,
36-
openSync,
37-
fstatSync,
38-
readFileSync,
39-
closeSync
4031
} = require('fs');
4132
const { getOptionValue } = require('internal/options');
42-
const { sep, relative, join } = require('path');
33+
const { sep, relative } = require('path');
34+
const { Module: CJSModule } = require('internal/modules/cjs/loader');
4335
const preserveSymlinks = getOptionValue('--preserve-symlinks');
4436
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
4537
const typeFlag = getOptionValue('--input-type');
@@ -55,6 +47,7 @@ const {
5547
ERR_UNSUPPORTED_ESM_URL_SCHEME,
5648
} = require('internal/errors').codes;
5749

50+
const packageJsonReader = require('internal/modules/package_json_reader');
5851
const DEFAULT_CONDITIONS = ObjectFreeze(['node', 'import']);
5952
const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS);
6053

@@ -70,6 +63,7 @@ function getConditionsSet(conditions) {
7063
}
7164

7265
const realpathCache = new SafeMap();
66+
const packageJSONCache = new SafeMap(); /* string -> PackageConfig */
7367

7468
function tryStatSync(path) {
7569
try {
@@ -79,6 +73,77 @@ function tryStatSync(path) {
7973
}
8074
}
8175

76+
function getPackageConfig(path) {
77+
const existing = packageJSONCache.get(path);
78+
if (existing !== undefined) {
79+
return existing;
80+
}
81+
const source = packageJsonReader.read(path).string;
82+
if (source === undefined) {
83+
const packageConfig = {
84+
exists: false,
85+
main: undefined,
86+
name: undefined,
87+
type: 'none',
88+
exports: undefined
89+
};
90+
packageJSONCache.set(path, packageConfig);
91+
return packageConfig;
92+
}
93+
94+
let packageJSON;
95+
try {
96+
packageJSON = JSONParse(source);
97+
} catch (error) {
98+
const errorPath = StringPrototypeSlice(path, 0, path.length - 13);
99+
throw new ERR_INVALID_PACKAGE_CONFIG(errorPath, error.message, true);
100+
}
101+
102+
let { main, name, type } = packageJSON;
103+
const { exports } = packageJSON;
104+
if (typeof main !== 'string') main = undefined;
105+
if (typeof name !== 'string') name = undefined;
106+
// Ignore unknown types for forwards compatibility
107+
if (type !== 'module' && type !== 'commonjs') type = 'none';
108+
109+
const packageConfig = {
110+
exists: true,
111+
main,
112+
name,
113+
type,
114+
exports
115+
};
116+
packageJSONCache.set(path, packageConfig);
117+
return packageConfig;
118+
}
119+
120+
function getPackageScopeConfig(resolved, base) {
121+
let packageJSONUrl = new URL('./package.json', resolved);
122+
while (true) {
123+
const packageJSONPath = packageJSONUrl.pathname;
124+
if (StringPrototypeEndsWith(packageJSONPath, 'node_modules/package.json'))
125+
break;
126+
const packageConfig = getPackageConfig(fileURLToPath(packageJSONUrl), base);
127+
if (packageConfig.exists) return packageConfig;
128+
129+
const lastPackageJSONUrl = packageJSONUrl;
130+
packageJSONUrl = new URL('../package.json', packageJSONUrl);
131+
132+
// Terminates at root where ../package.json equals ../../package.json
133+
// (can't just check "/package.json" for Windows support).
134+
if (packageJSONUrl.pathname === lastPackageJSONUrl.pathname) break;
135+
}
136+
const packageConfig = {
137+
exists: false,
138+
main: undefined,
139+
name: undefined,
140+
type: 'none',
141+
exports: undefined
142+
};
143+
packageJSONCache.set(fileURLToPath(packageJSONUrl), packageConfig);
144+
return packageConfig;
145+
}
146+
82147
/*
83148
* Legacy CommonJS main resolution:
84149
* 1. let M = pkg_url + (json main field)
@@ -331,7 +396,7 @@ function isConditionalExportsMainSugar(exports, packageJSONUrl, base) {
331396

332397

333398
function packageMainResolve(packageJSONUrl, packageConfig, base, conditions) {
334-
if (packageConfig) {
399+
if (packageConfig.exists) {
335400
const exports = packageConfig.exports;
336401
if (exports !== undefined) {
337402
if (isConditionalExportsMainSugar(exports, packageJSONUrl, base)) {
@@ -408,51 +473,9 @@ function packageExportsResolve(
408473
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
409474
}
410475

411-
function readIfFile(path) {
412-
let fd;
413-
try {
414-
fd = openSync(path, 'r');
415-
} catch {
416-
return undefined;
417-
}
418-
try {
419-
if (!fstatSync(fd).isFile()) return undefined;
420-
return readFileSync(fd, 'utf8');
421-
} finally {
422-
closeSync(fd);
423-
}
424-
}
425-
426-
function catchInvalidPackage(error, path) {
427-
if (error instanceof SyntaxError) {
428-
const packageJsonPath = join(path, 'package.json');
429-
const message = StringPrototypeSlice(
430-
error.message, ('Error parsing ' + packageJsonPath + ': ').length);
431-
throw new ERR_INVALID_PACKAGE_CONFIG(path, message, true);
432-
}
433-
throw error;
434-
}
435-
436-
function readPackageESM(path) {
437-
try {
438-
return readPackage(path, readIfFile);
439-
} catch (error) {
440-
catchInvalidPackage(error, path);
441-
}
442-
}
443-
444-
function readPackageScopeESM(url) {
445-
const path = fileURLToPath(url);
446-
try {
447-
return readPackageScope(path, readIfFile);
448-
} catch (error) {
449-
catchInvalidPackage(error, path);
450-
}
451-
}
452-
453476
function getPackageType(url) {
454-
const packageConfig = readPackageScopeESM(url);
455-
return packageConfig ? packageConfig.data.type : 'none';
477+
const packageConfig = getPackageScopeConfig(url, url);
478+
return packageConfig.type;
456479
}
457480

458481
/**
@@ -496,13 +519,12 @@ function packageResolve(specifier, base, conditions) {
496519
'' : '.' + StringPrototypeSlice(specifier, separatorIndex);
497520

498521
// ResolveSelf
499-
const packageScope = readPackageScopeESM(base);
500-
if (packageScope !== false) {
501-
const packageConfig = packageScope.data;
522+
const packageConfig = getPackageScopeConfig(base, base);
523+
if (packageConfig.exists) {
502524
// TODO(jkrems): Find a way to forward the pair/iterator already generated
503525
// while executing GetPackageScopeConfig
504526
let packageJSONUrl;
505-
for (const [ filename, packageConfigCandidate ] of packageJsonCache) {
527+
for (const [ filename, packageConfigCandidate ] of packageJSONCache) {
506528
if (packageConfig === packageConfigCandidate) {
507529
packageJSONUrl = pathToFileURL(filename);
508530
break;
@@ -540,15 +562,13 @@ function packageResolve(specifier, base, conditions) {
540562
}
541563

542564
// Package match.
543-
const packagePath = StringPrototypeSlice(
544-
packageJSONPath, 0, packageJSONPath.length - 13);
545-
const packageConfig = readPackageESM(packagePath);
565+
const packageConfig = getPackageConfig(packageJSONPath, base);
546566
if (packageSubpath === './') {
547567
return new URL('./', packageJSONUrl);
548568
} else if (packageSubpath === '') {
549569
return packageMainResolve(packageJSONUrl, packageConfig, base,
550570
conditions);
551-
} else if (packageConfig && packageConfig.exports !== undefined) {
571+
} else if (packageConfig.exports !== undefined) {
552572
return packageExportsResolve(
553573
packageJSONUrl, packageSubpath, packageConfig, base, conditions);
554574
} else {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use strict';
2+
3+
const { SafeMap } = primordials;
4+
const { internalModuleReadJSON } = internalBinding('fs');
5+
6+
const cache = new SafeMap();
7+
8+
/**
9+
*
10+
* @param {string} path
11+
*/
12+
function read(path) {
13+
if (cache.has(path)) {
14+
return cache.get(path);
15+
}
16+
17+
const result = internalModuleReadJSON(path);
18+
cache.set(path, result);
19+
return result;
20+
}
21+
22+
module.exports = { read };

node.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@
158158
'lib/internal/main/run_third_party_main.js',
159159
'lib/internal/main/worker_thread.js',
160160
'lib/internal/modules/run_main.js',
161+
'lib/internal/modules/package_json_reader.js',
161162
'lib/internal/modules/cjs/helpers.js',
162163
'lib/internal/modules/cjs/loader.js',
163164
'lib/internal/modules/esm/loader.js',

0 commit comments

Comments
 (0)