diff options
author | Guy Bedford <guybedford@gmail.com> | 2020-06-28 08:09:24 +0300 |
---|---|---|
committer | Guy Bedford <guybedford@gmail.com> | 2020-07-10 05:19:35 +0300 |
commit | 1237955d4149ce0d9445ca707bc824322fc8c15c (patch) | |
tree | f1511ef9cde4ad416e7431f0cb955664da529b77 /lib/internal/modules | |
parent | 1198aebd2db710b22bfed003edcf8cca2db8a174 (diff) |
module: package "imports" field
PR-URL: https://github.com/nodejs/node/pull/34117
Reviewed-By: Jan Krems <jan.krems@gmail.com>
Reviewed-By: Bradley Farias <bradley.meck@gmail.com>
Diffstat (limited to 'lib/internal/modules')
-rw-r--r-- | lib/internal/modules/cjs/loader.js | 74 | ||||
-rw-r--r-- | lib/internal/modules/esm/resolve.js | 230 |
2 files changed, 209 insertions, 95 deletions
diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index c2be5ea025a..5ffb4840632 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -21,6 +21,12 @@ 'use strict'; +// Set first due to cycle with ESM loader functions. +module.exports = { + wrapSafe, Module, toRealPath, readPackageScope, + get hasLoadedAnyUserCJSModule() { return hasLoadedAnyUserCJSModule; } +}; + const { ArrayIsArray, Error, @@ -40,6 +46,7 @@ const { RegExpPrototypeTest, SafeMap, SafeWeakMap, + SafeSet, String, StringPrototypeIndexOf, StringPrototypeLastIndexOf, @@ -95,24 +102,24 @@ const { const { validateString } = require('internal/validators'); const pendingDeprecation = getOptionValue('--pending-deprecation'); -module.exports = { - wrapSafe, Module, toRealPath, readPackageScope, - get hasLoadedAnyUserCJSModule() { return hasLoadedAnyUserCJSModule; } -}; - -let asyncESM, ModuleJob, ModuleWrap, kInstantiated; - const { CHAR_FORWARD_SLASH, CHAR_BACKWARD_SLASH, CHAR_COLON } = require('internal/constants'); - const { isProxy } = require('internal/util/types'); +const asyncESM = require('internal/process/esm_loader'); +const ModuleJob = require('internal/modules/esm/module_job'); +const { ModuleWrap, kInstantiated } = internalBinding('module_wrap'); +const { + encodedSepRegEx, + packageInternalResolve +} = require('internal/modules/esm/resolve'); + const isWindows = process.platform === 'win32'; const relativeResolveCache = ObjectCreate(null); @@ -287,6 +294,7 @@ function readPackage(requestPath) { name: parsed.name, main: parsed.main, exports: parsed.exports, + imports: parsed.imports, type: parsed.type }; packageJsonCache.set(jsonPath, filtered); @@ -561,21 +569,26 @@ function resolveExportsTarget(baseUrl, target, subpath, mappingKey) { if (subpath.length > 0 && target[target.length - 1] !== '/') resolvedTarget = undefined; if (resolvedTarget === undefined) - throw new ERR_INVALID_PACKAGE_TARGET(StringPrototypeSlice(baseUrl.pathname - , 0, -1), mappingKey, subpath, target); + throw new ERR_INVALID_PACKAGE_TARGET(baseUrl.pathname, mappingKey, + target); const resolved = new URL(subpath, resolvedTarget); const resolvedPath = resolved.pathname; if (StringPrototypeStartsWith(resolvedPath, resolvedTargetPath) && StringPrototypeIndexOf(resolvedPath, '/node_modules/', pkgPathPath.length - 1) === -1) { + if (StringPrototypeMatch(resolvedPath, encodedSepRegEx)) + throw new ERR_INVALID_MODULE_SPECIFIER( + resolvedPath, 'must not include encoded "/" or "\\" characters', + fileURLToPath(baseUrl)); return fileURLToPath(resolved); } - throw new ERR_INVALID_MODULE_SPECIFIER(StringPrototypeSlice(baseUrl.pathname - , 0, -1), mappingKey); + const reason = 'request is not a valid subpath for the "exports" ' + + `resolution of ${baseUrl.pathname}package.json`; + throw new ERR_INVALID_MODULE_SPECIFIER(mappingKey + subpath, reason); } else if (ArrayIsArray(target)) { if (target.length === 0) throw new ERR_PACKAGE_PATH_NOT_EXPORTED( - StringPrototypeSlice(baseUrl.pathname, 0, -1), mappingKey + subpath); + baseUrl.pathname, mappingKey + subpath); let lastException; for (const targetValue of target) { try { @@ -617,13 +630,12 @@ function resolveExportsTarget(baseUrl, target, subpath, mappingKey) { } } throw new ERR_PACKAGE_PATH_NOT_EXPORTED( - StringPrototypeSlice(baseUrl.pathname, 0, -1), mappingKey + subpath); + baseUrl.pathname, mappingKey + subpath); } else if (target === null) { throw new ERR_PACKAGE_PATH_NOT_EXPORTED( - StringPrototypeSlice(baseUrl.pathname, 0, -1), mappingKey + subpath); + baseUrl.pathname, mappingKey + subpath); } - throw new ERR_INVALID_PACKAGE_TARGET( - StringPrototypeSlice(baseUrl.pathname, 0, -1), mappingKey, subpath, target); + throw new ERR_INVALID_PACKAGE_TARGET(baseUrl.pathname, mappingKey, target); } const trailingSlashRegex = /(?:^|\/)\.?\.$/; @@ -978,6 +990,8 @@ Module._load = function(request, parent, isMain) { return module.exports; }; +// TODO: Use this set when resolving pkg#exports conditions. +const cjsConditions = new SafeSet(['require', 'node']); Module._resolveFilename = function(request, parent, isMain, options) { if (NativeModule.canBeRequiredByUsers(request)) { return request; @@ -1020,6 +1034,27 @@ Module._resolveFilename = function(request, parent, isMain, options) { } if (parent && parent.filename) { + if (request[0] === '#') { + const pkg = readPackageScope(parent.filename) || {}; + if (pkg.data && pkg.data.imports !== null && + pkg.data.imports !== undefined) { + try { + const resolved = packageInternalResolve( + request, pathToFileURL(parent.filename), cjsConditions); + return fileURLToPath(resolved); + } catch (err) { + if (err.code === 'ERR_MODULE_NOT_FOUND') { + // eslint-disable-next-line no-restricted-syntax + const err = new Error(`Cannot find module '${request}'`); + err.code = 'MODULE_NOT_FOUND'; + err.path = path.resolve(pkg.path, 'package.json'); + // TODO(BridgeAR): Add the requireStack as well. + throw err; + } + throw err; + } + } + } const filename = trySelf(parent.filename, request); if (filename) { const cacheKey = request + '\x00' + @@ -1373,8 +1408,3 @@ Module.syncBuiltinESMExports = function syncBuiltinESMExports() { // Backwards compatibility Module.Module = Module; - -// We have to load the esm things after module.exports! -asyncESM = require('internal/process/esm_loader'); -ModuleJob = require('internal/modules/esm/module_job'); -({ ModuleWrap, kInstantiated } = internalBinding('module_wrap')); diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index 987a139c6aa..16ca78880c7 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -10,6 +10,7 @@ const { ObjectGetOwnPropertyNames, ObjectPrototypeHasOwnProperty, RegExp, + RegExpPrototypeTest, SafeMap, SafeSet, String, @@ -22,7 +23,6 @@ const { StringPrototypeStartsWith, StringPrototypeSubstr, } = primordials; -const assert = require('internal/assert'); const internalFS = require('internal/fs/utils'); const { NativeModule } = require('internal/bootstrap/loaders'); const { @@ -44,6 +44,7 @@ const { ERR_INVALID_PACKAGE_CONFIG, ERR_INVALID_PACKAGE_TARGET, ERR_MODULE_NOT_FOUND, + ERR_PACKAGE_IMPORT_NOT_DEFINED, ERR_PACKAGE_PATH_NOT_EXPORTED, ERR_UNSUPPORTED_DIR_IMPORT, ERR_UNSUPPORTED_ESM_URL_SCHEME, @@ -91,11 +92,13 @@ function getPackageConfig(path) { const source = packageJsonReader.read(path).string; if (source === undefined) { const packageConfig = { + pjsonPath: path, exists: false, main: undefined, name: undefined, type: 'none', - exports: undefined + exports: undefined, + imports: undefined, }; packageJSONCache.set(path, packageConfig); return packageConfig; @@ -109,19 +112,22 @@ function getPackageConfig(path) { throw new ERR_INVALID_PACKAGE_CONFIG(errorPath, error.message, true); } - let { main, name, type } = packageJSON; + 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 + exports, + imports, }; packageJSONCache.set(path, packageConfig); return packageConfig; @@ -143,14 +149,17 @@ function getPackageScopeConfig(resolved, base) { // (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 + exports: undefined, + imports: undefined, }; - packageJSONCache.set(fileURLToPath(packageJSONUrl), packageConfig); + packageJSONCache.set(packageJSONPath, packageConfig); return packageConfig; } @@ -233,6 +242,7 @@ function resolveIndex(search) { return resolveExtensions(new URL('index', search)); } +const encodedSepRegEx = /%2F|%2C/i; function finalizeResolution(resolved, base) { if (getOptionValue('--experimental-specifier-resolution') === 'node') { let file = resolveExtensionsWithTryExactName(resolved); @@ -247,6 +257,11 @@ function finalizeResolution(resolved, base) { resolved.pathname, fileURLToPath(base), 'module'); } + if (RegExpPrototypeTest(encodedSepRegEx, resolved.pathname)) + throw new ERR_INVALID_MODULE_SPECIFIER( + resolved.pathname, 'must not include encoded "/" or "\\" characters', + fileURLToPath(base)); + const path = fileURLToPath(resolved); const stats = tryStatSync(path); @@ -263,34 +278,52 @@ function finalizeResolution(resolved, base) { return resolved; } +function throwImportNotDefined(specifier, packageJSONUrl, base) { + throw new ERR_PACKAGE_IMPORT_NOT_DEFINED( + specifier, packageJSONUrl && fileURLToPath(new URL('.', packageJSONUrl)), + fileURLToPath(base)); +} + function throwExportsNotFound(subpath, packageJSONUrl, base) { throw new ERR_PACKAGE_PATH_NOT_EXPORTED( - fileURLToPath(packageJSONUrl), subpath, fileURLToPath(base)); + fileURLToPath(new URL('.', packageJSONUrl)), subpath, fileURLToPath(base)); } -function throwSubpathInvalid(subpath, packageJSONUrl, base) { - throw new ERR_INVALID_MODULE_SPECIFIER( - fileURLToPath(packageJSONUrl), subpath, fileURLToPath(base)); +function throwInvalidSubpath(subpath, packageJSONUrl, internal, base) { + const reason = `request is not a valid subpath for the "${internal ? + 'imports' : 'exports'}" resolution of ${fileURLToPath(packageJSONUrl)}${ + base ? ` imported from ${base}` : ''}`; + throw new ERR_INVALID_MODULE_SPECIFIER(subpath, reason, fileURLToPath(base)); } -function throwExportsInvalid( - subpath, target, packageJSONUrl, base) { +function throwInvalidPackageTarget( + subpath, target, packageJSONUrl, internal, base) { if (typeof target === 'object' && target !== null) { target = JSONStringify(target, null, ''); - } else if (ArrayIsArray(target)) { - target = `[${target}]`; } else { target = `${target}`; } throw new ERR_INVALID_PACKAGE_TARGET( - fileURLToPath(packageJSONUrl), null, subpath, target, fileURLToPath(base)); + fileURLToPath(new URL('.', packageJSONUrl)), subpath, target, + internal, fileURLToPath(base)); } -function resolveExportsTargetString( - target, subpath, match, packageJSONUrl, base) { - if (target[0] !== '.' || target[1] !== '/' || - (subpath !== '' && target[target.length - 1] !== '/')) { - throwExportsInvalid(match, target, packageJSONUrl, base); +function resolvePackageTargetString( + target, subpath, match, packageJSONUrl, base, internal, conditions) { + if (subpath !== '' && target[target.length - 1] !== '/') + throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); + + if (!target.startsWith('./')) { + if (internal && !target.startsWith('../') && !target.startsWith('/')) { + let isURL = false; + try { + new URL(target); + isURL = true; + } catch {} + if (!isURL) + return packageResolve(target + subpath, base, conditions); + } + throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); } const resolved = new URL(target, packageJSONUrl); @@ -299,18 +332,16 @@ function resolveExportsTargetString( if (!StringPrototypeStartsWith(resolvedPath, packagePath) || StringPrototypeIncludes( - resolvedPath, '/node_modules/', packagePath.length - 1)) { - throwExportsInvalid(match, target, packageJSONUrl, base); - } + resolvedPath, '/node_modules/', packagePath.length - 1)) + throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); if (subpath === '') return resolved; const subpathResolved = new URL(subpath, resolved); const subpathResolvedPath = subpathResolved.pathname; if (!StringPrototypeStartsWith(subpathResolvedPath, resolvedPath) || StringPrototypeIncludes(subpathResolvedPath, - '/node_modules/', packagePath.length - 1)) { - throwSubpathInvalid(match + subpath, packageJSONUrl, base); - } + '/node_modules/', packagePath.length - 1)) + throwInvalidSubpath(match + subpath, packageJSONUrl, internal, base); return subpathResolved; } @@ -324,36 +355,43 @@ function isArrayIndex(key) { return keyNum >= 0 && keyNum < 0xFFFF_FFFF; } -function resolveExportsTarget( - packageJSONUrl, target, subpath, packageSubpath, base, conditions) { +function resolvePackageTarget( + packageJSONUrl, target, subpath, packageSubpath, base, internal, conditions) { if (typeof target === 'string') { - const resolved = resolveExportsTargetString( - target, subpath, packageSubpath, packageJSONUrl, base); + const resolved = resolvePackageTargetString( + target, subpath, packageSubpath, packageJSONUrl, base, internal, + conditions); + if (resolved === null) + return null; return finalizeResolution(resolved, base); } else if (ArrayIsArray(target)) { if (target.length === 0) - throwExportsNotFound(packageSubpath, packageJSONUrl, base); + return null; let lastException; for (let i = 0; i < target.length; i++) { const targetItem = target[i]; let resolved; try { - resolved = resolveExportsTarget( - packageJSONUrl, targetItem, subpath, packageSubpath, base, + resolved = resolvePackageTarget( + packageJSONUrl, targetItem, subpath, packageSubpath, base, internal, conditions); } catch (e) { lastException = e; - if (e.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' || - e.code === 'ERR_INVALID_PACKAGE_TARGET') { + if (e.code === 'ERR_INVALID_PACKAGE_TARGET') continue; - } throw e; } - + if (resolved === undefined) + continue; + if (resolved === null) { + lastException = null; + continue; + } return finalizeResolution(resolved, base); } - assert(lastException !== undefined); + if (lastException === undefined || lastException === null) + return lastException; throw lastException; } else if (typeof target === 'object' && target !== null) { const keys = ObjectGetOwnPropertyNames(target); @@ -369,21 +407,20 @@ function resolveExportsTarget( const key = keys[i]; if (key === 'default' || conditions.has(key)) { const conditionalTarget = target[key]; - try { - return resolveExportsTarget( - packageJSONUrl, conditionalTarget, subpath, packageSubpath, base, - conditions); - } catch (e) { - if (e.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED') continue; - throw e; - } + const resolved = resolvePackageTarget( + packageJSONUrl, conditionalTarget, subpath, packageSubpath, base, + internal, conditions); + if (resolved === undefined) + continue; + return resolved; } } - throwExportsNotFound(packageSubpath, packageJSONUrl, base); + return undefined; } else if (target === null) { - throwExportsNotFound(packageSubpath, packageJSONUrl, base); + return null; } - throwExportsInvalid(packageSubpath, target, packageJSONUrl, base); + throwInvalidPackageTarget(packageSubpath, target, packageJSONUrl, internal, + base); } function isConditionalExportsMainSugar(exports, packageJSONUrl, base) { @@ -409,19 +446,25 @@ function isConditionalExportsMainSugar(exports, packageJSONUrl, base) { return isConditionalSugar; } - function packageMainResolve(packageJSONUrl, packageConfig, base, conditions) { if (packageConfig.exists) { const exports = packageConfig.exports; if (exports !== undefined) { if (isConditionalExportsMainSugar(exports, packageJSONUrl, base)) { - return resolveExportsTarget(packageJSONUrl, exports, '', '', base, - conditions); + const resolved = resolvePackageTarget(packageJSONUrl, exports, '', '', + base, false, conditions); + if (resolved === null || resolved === undefined) + throwExportsNotFound('.', packageJSONUrl, base); + return resolved; } else if (typeof exports === 'object' && exports !== null) { const target = exports['.']; - if (target !== undefined) - return resolveExportsTarget(packageJSONUrl, target, '', '', base, - conditions); + if (target !== undefined) { + const resolved = resolvePackageTarget(packageJSONUrl, target, '', '', + base, false, conditions); + if (resolved === null || resolved === undefined) + throwExportsNotFound('.', packageJSONUrl, base); + return resolved; + } } throw new ERR_PACKAGE_PATH_NOT_EXPORTED(packageJSONUrl, '.'); @@ -457,11 +500,12 @@ function packageExportsResolve( throwExportsNotFound(packageSubpath, packageJSONUrl, base); } - if (ObjectPrototypeHasOwnProperty(exports, packageSubpath)) { const target = exports[packageSubpath]; - const resolved = resolveExportsTarget( - packageJSONUrl, target, '', packageSubpath, base, conditions); + const resolved = resolvePackageTarget( + packageJSONUrl, target, '', packageSubpath, base, false, conditions); + if (resolved === null || resolved === undefined) + throwExportsNotFound(packageSubpath, packageJSONUrl, base); return finalizeResolution(resolved, base); } @@ -479,14 +523,59 @@ function packageExportsResolve( if (bestMatch) { const target = exports[bestMatch]; const subpath = StringPrototypeSubstr(packageSubpath, bestMatch.length); - const resolved = resolveExportsTarget( - packageJSONUrl, target, subpath, packageSubpath, base, conditions); + const resolved = resolvePackageTarget( + packageJSONUrl, target, subpath, bestMatch, base, false, conditions); + if (resolved === null || resolved === undefined) + throwExportsNotFound(packageSubpath, packageJSONUrl, base); return finalizeResolution(resolved, base); } throwExportsNotFound(packageSubpath, packageJSONUrl, base); } +function packageInternalResolve(name, base, conditions) { + if (name === '#' || name.startsWith('#/')) { + const reason = 'is not a valid internal imports specifier name'; + throw new ERR_INVALID_MODULE_SPECIFIER(name, reason, fileURLToPath(base)); + } + let packageJSONUrl; + const packageConfig = getPackageScopeConfig(base, base); + if (packageConfig.exists) { + packageJSONUrl = pathToFileURL(packageConfig.pjsonPath); + const imports = packageConfig.imports; + if (imports) { + if (ObjectPrototypeHasOwnProperty(imports, name)) { + const resolved = resolvePackageTarget( + packageJSONUrl, imports[name], '', name, base, true, conditions); + if (resolved !== null) + return finalizeResolution(resolved, base); + } else { + let bestMatch = ''; + const keys = ObjectGetOwnPropertyNames(imports); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (key[key.length - 1] !== '/') continue; + if (StringPrototypeStartsWith(name, key) && + key.length > bestMatch.length) { + bestMatch = key; + } + } + + if (bestMatch) { + const target = imports[bestMatch]; + const subpath = StringPrototypeSubstr(name, bestMatch.length); + const resolved = resolvePackageTarget( + packageJSONUrl, target, subpath, bestMatch, base, true, + conditions); + if (resolved !== null) + return finalizeResolution(resolved, base); + } + } + } + } + throwImportNotDefined(name, packageJSONUrl, base); +} + function getPackageType(url) { const packageConfig = getPackageScopeConfig(url, url); return packageConfig.type; @@ -526,7 +615,7 @@ function packageResolve(specifier, base, conditions) { if (!validPackageName) { throw new ERR_INVALID_MODULE_SPECIFIER( - specifier, undefined, fileURLToPath(base)); + specifier, 'is not a valid package name', fileURLToPath(base)); } const packageSubpath = separatorIndex === -1 ? @@ -535,17 +624,8 @@ function packageResolve(specifier, base, conditions) { // ResolveSelf const packageConfig = getPackageScopeConfig(base, base); if (packageConfig.exists) { - // TODO(jkrems): Find a way to forward the pair/iterator already generated - // while executing GetPackageScopeConfig - let packageJSONUrl; - for (const [ filename, packageConfigCandidate ] of packageJSONCache) { - if (packageConfig === packageConfigCandidate) { - packageJSONUrl = pathToFileURL(filename); - break; - } - } - if (packageJSONUrl !== undefined && - packageConfig.name === packageName && + const packageJSONUrl = pathToFileURL(packageConfig.pjsonPath); + if (packageConfig.name === packageName && packageConfig.exports !== undefined) { if (packageSubpath === './') { return new URL('./', packageJSONUrl); @@ -626,6 +706,8 @@ function moduleResolve(specifier, base, conditions) { let resolved; if (shouldBeTreatedAsRelativeOrAbsolutePath(specifier)) { resolved = new URL(specifier, base); + } else if (specifier[0] === '#') { + resolved = packageInternalResolve(specifier, base, conditions); } else { try { resolved = new URL(specifier); @@ -764,5 +846,7 @@ function defaultResolve(specifier, context = {}, defaultResolveUnused) { module.exports = { DEFAULT_CONDITIONS, defaultResolve, - getPackageType + encodedSepRegEx, + getPackageType, + packageInternalResolve }; |