diff --git a/lib/internal/modules/esm/package_config.js b/lib/internal/modules/esm/package_config.js new file mode 100644 index 00000000000000..89e90d0d997cd2 --- /dev/null +++ b/lib/internal/modules/esm/package_config.js @@ -0,0 +1,142 @@ +'use strict'; + +const { + JSONParse, + SafeMap, + StringPrototypeEndsWith, +} = primordials; +const { URL, fileURLToPath } = require('internal/url'); +const { + ERR_INVALID_PACKAGE_CONFIG, +} = require('internal/errors').codes; + +const packageJsonReader = require('internal/modules/package_json_reader'); + + +/** + * @typedef {string | string[] | Record} Exports + * @typedef {'module' | 'commonjs'} PackageType + * @typedef {{ + * pjsonPath: string, + * exports?: ExportConfig, + * name?: string, + * main?: string, + * type?: PackageType, + * }} PackageConfig + */ + +/** @type {Map} */ +const packageJSONCache = new SafeMap(); + + +/** + * @param {string} path + * @param {string} specifier + * @param {string | URL | undefined} base + * @returns {PackageConfig} + */ +function getPackageConfig(path, specifier, base) { + const existing = packageJSONCache.get(path); + if (existing !== undefined) { + return existing; + } + const source = packageJsonReader.read(path).string; + if (source === undefined) { + const packageConfig = { + pjsonPath: path, + exists: false, + main: undefined, + name: undefined, + type: 'none', + exports: undefined, + imports: undefined, + }; + packageJSONCache.set(path, packageConfig); + return packageConfig; + } + + let packageJSON; + try { + packageJSON = JSONParse(source); + } catch (error) { + throw new ERR_INVALID_PACKAGE_CONFIG( + path, + (base ? `"${specifier}" from ` : '') + fileURLToPath(base || specifier), + error.message + ); + } + + let { imports, main, name, type } = packageJSON; + const { exports } = packageJSON; + if (typeof imports !== 'object' || imports === null) { + imports = undefined; + } + if (typeof main !== 'string') { + main = undefined; + } + if (typeof name !== 'string') { + name = undefined; + } + // Ignore unknown types for forwards compatibility + if (type !== 'module' && type !== 'commonjs') { + type = 'none'; + } + + const packageConfig = { + pjsonPath: path, + exists: true, + main, + name, + type, + exports, + imports, + }; + packageJSONCache.set(path, packageConfig); + return packageConfig; +} + + +/** + * @param {URL | string} resolved + * @returns {PackageConfig} + */ +function getPackageScopeConfig(resolved) { + let packageJSONUrl = new URL('./package.json', resolved); + while (true) { + const packageJSONPath = packageJSONUrl.pathname; + if (StringPrototypeEndsWith(packageJSONPath, 'node_modules/package.json')) { + break; + } + const packageConfig = getPackageConfig(fileURLToPath(packageJSONUrl), resolved); + if (packageConfig.exists) { + return packageConfig; + } + + const lastPackageJSONUrl = packageJSONUrl; + packageJSONUrl = new URL('../package.json', packageJSONUrl); + + // Terminates at root where ../package.json equals ../../package.json + // (can't just check "/package.json" for Windows support). + if (packageJSONUrl.pathname === lastPackageJSONUrl.pathname) { + break; + } + } + const packageJSONPath = fileURLToPath(packageJSONUrl); + const packageConfig = { + pjsonPath: packageJSONPath, + exists: false, + main: undefined, + name: undefined, + type: 'none', + exports: undefined, + imports: undefined, + }; + packageJSONCache.set(packageJSONPath, packageConfig); + return packageConfig; +} + + +module.exports = { + getPackageConfig, + getPackageScopeConfig, +}; diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index fc5dcd6863dc35..a3b4b1946d371d 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -59,9 +59,16 @@ const { ERR_NETWORK_IMPORT_DISALLOWED, ERR_UNSUPPORTED_ESM_URL_SCHEME, } = require('internal/errors').codes; -const { Module: CJSModule } = require('internal/modules/cjs/loader'); +const { Module: CJSModule } = require('internal/modules/cjs/loader'); const packageJsonReader = require('internal/modules/package_json_reader'); +const { getPackageConfig, getPackageScopeConfig } = require('internal/modules/esm/package_config'); + +/** + * @typedef {import('internal/modules/esm/package_config.js').PackageConfig} PackageConfig + */ + + const userConditions = getOptionValue('--conditions'); const noAddons = getOptionValue('--no-addons'); const addonConditions = noAddons ? [] : ['node-addons']; @@ -75,18 +82,6 @@ const DEFAULT_CONDITIONS = ObjectFreeze([ const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS); -/** - * @typedef {string | string[] | Record} Exports - * @typedef {'module' | 'commonjs'} PackageType - * @typedef {{ - * pjsonPath: string, - * exports?: ExportConfig, - * name?: string, - * main?: string, - * type?: PackageType, - * }} PackageConfig - */ - const emittedPackageWarnings = new SafeSet(); /** @@ -179,7 +174,6 @@ function getConditionsSet(conditions) { } const realpathCache = new SafeMap(); -const packageJSONCache = new SafeMap(); /* string -> PackageConfig */ /** * @param {string | URL} path @@ -188,99 +182,6 @@ const packageJSONCache = new SafeMap(); /* string -> PackageConfig */ const tryStatSync = (path) => statSync(path, { throwIfNoEntry: false }) ?? new Stats(); -/** - * @param {string} path - * @param {string} specifier - * @param {string | URL | undefined} base - * @returns {PackageConfig} - */ -function getPackageConfig(path, specifier, base) { - const existing = packageJSONCache.get(path); - if (existing !== undefined) { - return existing; - } - const source = packageJsonReader.read(path).string; - if (source === undefined) { - const packageConfig = { - pjsonPath: path, - exists: false, - main: undefined, - name: undefined, - type: 'none', - exports: undefined, - imports: undefined, - }; - packageJSONCache.set(path, packageConfig); - return packageConfig; - } - - let packageJSON; - try { - packageJSON = JSONParse(source); - } catch (error) { - throw new ERR_INVALID_PACKAGE_CONFIG( - path, - (base ? `"${specifier}" from ` : '') + fileURLToPath(base || specifier), - error.message - ); - } - - let { imports, main, name, type } = packageJSON; - const { exports } = packageJSON; - if (typeof imports !== 'object' || imports === null) imports = undefined; - if (typeof main !== 'string') main = undefined; - if (typeof name !== 'string') name = undefined; - // Ignore unknown types for forwards compatibility - if (type !== 'module' && type !== 'commonjs') type = 'none'; - - const packageConfig = { - pjsonPath: path, - exists: true, - main, - name, - type, - exports, - imports, - }; - packageJSONCache.set(path, packageConfig); - return packageConfig; -} - -/** - * @param {URL | string} resolved - * @returns {PackageConfig} - */ -function getPackageScopeConfig(resolved) { - let packageJSONUrl = new URL('./package.json', resolved); - while (true) { - const packageJSONPath = packageJSONUrl.pathname; - if (StringPrototypeEndsWith(packageJSONPath, 'node_modules/package.json')) - break; - const packageConfig = getPackageConfig(fileURLToPath(packageJSONUrl), - resolved); - if (packageConfig.exists) return packageConfig; - - const lastPackageJSONUrl = packageJSONUrl; - packageJSONUrl = new URL('../package.json', packageJSONUrl); - - // Terminates at root where ../package.json equals ../../package.json - // (can't just check "/package.json" for Windows support). - if (packageJSONUrl.pathname === lastPackageJSONUrl.pathname) break; - } - const packageJSONPath = fileURLToPath(packageJSONUrl); - const packageConfig = { - pjsonPath: packageJSONPath, - exists: false, - main: undefined, - name: undefined, - type: 'none', - exports: undefined, - imports: undefined, - }; - packageJSONCache.set(packageJSONPath, packageConfig); - return packageConfig; -} - /** * @param {string | URL} url * @returns {boolean} @@ -634,7 +535,7 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath, /** * - * @param {Exports} exports + * @param {import('internal/modules/esm/package_config.js').Exports} exports * @param {URL} packageJSONUrl * @param {string | URL | undefined} base * @returns {boolean} @@ -838,7 +739,7 @@ function packageImportsResolve(name, base, conditions) { /** * @param {URL} url - * @returns {PackageType} + * @returns {import('internal/modules/esm/package_config.js').PackageType} */ function getPackageType(url) { const packageConfig = getPackageScopeConfig(url); diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index b10fe5c2ff078d..c5da4d386f9ffe 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -84,6 +84,7 @@ const expectedModules = new Set([ 'NativeModule internal/modules/esm/loader', 'NativeModule internal/modules/esm/module_job', 'NativeModule internal/modules/esm/module_map', + 'NativeModule internal/modules/esm/package_config', 'NativeModule internal/modules/esm/resolve', 'NativeModule internal/modules/esm/translators', 'NativeModule internal/modules/package_json_reader',