From ac7d34f59f0ee8cd5a0a5c9195e2ec1dc25579eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrg=C3=BCn=20Day=C4=B1o=C4=9Flu?= Date: Sat, 24 Oct 2026 16:27:13 +0200 Subject: [PATCH] module: avoid reparsing package.json in esm format detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Determining the format of a `.js` or `.ts` module called `getPackageScopeConfig()`, which spread the deserialized package configuration and thereby invoked its lazy `exports` and `imports` getters. Every resolved module parsed those maps again, so load time and memory grew with the number of modules in a package times the size of its `exports` map. Use `getPackageType()` for format detection and only look up the package.json path when warning about a typeless package. Stop spreading the configuration in `getPackageScopeConfig()` so that the maps are parsed only when accessed. Fixes: https://github.com/nodejs/node/issues/66485 Signed-off-by: Gürgün Dayıoğlu --- lib/internal/modules/esm/get_format.js | 28 ++++++++++++--------- lib/internal/modules/package_json_reader.js | 11 ++++---- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/lib/internal/modules/esm/get_format.js b/lib/internal/modules/esm/get_format.js index e69a0a01d996..0b7e1ed438ed 100644 --- a/lib/internal/modules/esm/get_format.js +++ b/lib/internal/modules/esm/get_format.js @@ -149,9 +149,17 @@ function underNodeModules(url) { } let typelessPackageJsonFilesWarnedAbout; -function warnTypelessPackageJsonFile(pjsonPath, url) { +/** + * Called for a module with a package.json without a `type` field (or none at all) and ESM syntax. + * Warns about the missing `type` field so that the user can avoid the performance penalty of detection. + * @param {URL} url + */ +function warnIfTypelessPackageJsonFile(url) { + if (underNodeModules(url)) { return; } + const { pjsonPath, exists } = getPackageScopeConfig(url); + if (!exists) { return; } typelessPackageJsonFilesWarnedAbout ??= new SafeSet(); - if (!underNodeModules(url) && !typelessPackageJsonFilesWarnedAbout.has(pjsonPath)) { + if (!typelessPackageJsonFilesWarnedAbout.has(pjsonPath)) { const warning = `Module type of ${url} is not specified and it doesn't parse as CommonJS.\n` + 'Reparsing as ES module because module syntax was detected. This incurs a performance overhead.\n' + `To eliminate this warning, add "type": "module" to ${pjsonPath}.`; @@ -173,7 +181,7 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE const ext = extname(url); if (ext === '.js') { - const { type: packageType, pjsonPath, exists: foundPackageJson } = getPackageScopeConfig(url); + const packageType = getPackageType(url); if (packageType !== 'none') { return packageType; } @@ -183,15 +191,13 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE // but this gets called again from `defaultLoad`/`defaultLoadSync`. // For ambiguous files (.js, no type field) we return undefined from `resolve` and re-run the check in `load`. const format = detectModuleFormat(source, url); - if (format === 'module' && foundPackageJson) { - // This module has a .js extension, a package.json with no `type` field, and ESM syntax. - // Warn about the missing `type` field so that the user can avoid the performance penalty of detection. - warnTypelessPackageJsonFile(pjsonPath, url); + if (format === 'module') { + warnIfTypelessPackageJsonFile(url); } return format; } if (ext === '.ts' && getOptionValue('--strip-types')) { - const { type: packageType, pjsonPath, exists: foundPackageJson } = getPackageScopeConfig(url); + const packageType = getPackageType(url); if (packageType !== 'none') { return `${packageType}-typescript`; } @@ -206,10 +212,8 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE const parsedSource = stripTypeScriptModuleTypes(stringifiedSource, fileURLToPath(url)); const detectedFormat = detectModuleFormat(parsedSource, url); const format = `${detectedFormat}-typescript`; - if (format === 'module-typescript' && foundPackageJson) { - // This module has a .js extension, a package.json with no `type` field, and ESM syntax. - // Warn about the missing `type` field so that the user can avoid the performance penalty of detection. - warnTypelessPackageJsonFile(pjsonPath, url); + if (format === 'module-typescript') { + warnIfTypelessPackageJsonFile(url); } return format; } diff --git a/lib/internal/modules/package_json_reader.js b/lib/internal/modules/package_json_reader.js index 2ef5d1ebed39..e4455da046a2 100644 --- a/lib/internal/modules/package_json_reader.js +++ b/lib/internal/modules/package_json_reader.js @@ -264,12 +264,11 @@ function getPackageScopeConfig(resolved) { if (ArrayIsArray(result)) { const { data, exists, path } = deserializePackageJSON(`${resolved}`, result); - return { - __proto__: null, - ...data, - exists, - pjsonPath: path, - }; + // `data` is not shared, so extend it in place instead of spreading it: + // spreading would invoke the lazy `imports` and `exports` getters. + data.exists = exists; + data.pjsonPath = path; + return data; } // This means that the response is a string