const { satisfies } = require('bare-semver') const errors = require('./lib/errors') module.exports = exports = function resolve(specifier, parentURL, opts, readPackage) { if (typeof opts === 'function') { readPackage = opts opts = {} } else if (typeof readPackage !== 'function') { readPackage = defaultReadPackage } return { *[Symbol.iterator]() { const generator = exports.module(specifier, parentURL, opts) let next = generator.next() while (next.done !== true) { const value = next.value if (value.package) { next = generator.next(readPackage(value.package)) } else { next = generator.next(yield value.resolution) } } return next.value }, async *[Symbol.asyncIterator]() { const generator = exports.module(specifier, parentURL, opts) let next = generator.next() while (next.done !== true) { const value = next.value if (value.package) { next = generator.next(await readPackage(value.package)) } else { next = generator.next(yield value.resolution) } } return next.value } } } function defaultReadPackage() { return null } // No resolution candidate was yielded const UNRESOLVED = 0x0 // At least 1 resolution candidate was yielded const YIELDED = 0x1 // At least 1 resolution candidate was yielded and resolved const RESOLVED = YIELDED | 0x2 exports.constants = { UNRESOLVED, YIELDED, RESOLVED } exports.module = function* (specifier, parentURL, opts = {}) { const { resolutions = null, imports = null } = opts if (exports.startsWithWindowsDriveLetter(specifier)) { specifier = '/' + specifier } let status if (resolutions) { status = yield* exports.preresolved(specifier, resolutions, parentURL, opts) if (status) return status } status = yield* exports.url(specifier, parentURL, opts) if (status) return status status = yield* exports.packageImports(specifier, parentURL, opts) if (status) return status if ( specifier === '.' || specifier === '..' || specifier[0] === '/' || specifier[0] === '\\' || specifier.startsWith('./') || specifier.startsWith('.\\') || specifier.startsWith('../') || specifier.startsWith('..\\') ) { if (imports) { status = yield* exports.packageImportsExports(specifier, imports, parentURL, true, opts) if (status) return status } status = yield* exports.deferred(specifier, opts) if (status) return status status = yield* exports.file(specifier, parentURL, false, opts) if (status === RESOLVED) return status return yield* exports.directory(specifier, parentURL, opts) } return yield* exports.package(specifier, parentURL, opts) } exports.url = function* (url, parentURL, opts = {}) { const { imports = null, deferredProtocol = 'deferred:', resolutions = null } = opts let resolution try { resolution = new URL(url) } catch { return UNRESOLVED } if (imports) { const status = yield* exports.packageImportsExports( resolution.href, imports, parentURL, true, opts ) if (status) return status } if (resolution.protocol === deferredProtocol) { const specifier = resolution.pathname if (resolutions) { const imports = resolutions[parentURL.href] if (typeof imports === 'object' && imports !== null) { opts = { ...opts, resolutions: { ...resolutions, [parentURL.href]: { ...imports, [specifier]: null } } } } } return yield* exports.module(specifier, parentURL, opts) } if (resolution.protocol === 'node:') { const specifier = resolution.pathname if ( specifier === '.' || specifier === '..' || specifier[0] === '/' || specifier.startsWith('./') || specifier.startsWith('../') ) { throw errors.INVALID_MODULE_SPECIFIER(`Module specifier '${url}' is not a valid package name`) } return yield* exports.package(specifier, parentURL, opts) } const resolved = yield { resolution } return resolved ? RESOLVED : YIELDED } exports.preresolved = function* (specifier, resolutions, parentURL, opts = {}) { const imports = resolutions[parentURL.href] if (typeof imports === 'object' && imports !== null) { return yield* exports.packageImportsExports(specifier, imports, parentURL, true, opts) } return UNRESOLVED } exports.deferred = function* (specifier, opts = {}) { const { deferredProtocol = 'deferred:', defer = [] } = opts if (defer.includes(specifier)) { const resolved = yield { resolution: new URL(deferredProtocol + specifier) } return resolved ? RESOLVED : YIELDED } return UNRESOLVED } exports.package = function* (packageSpecifier, parentURL, opts = {}) { const { builtins = [] } = opts if (packageSpecifier === '') { throw errors.INVALID_MODULE_SPECIFIER( `Module specifier '${packageSpecifier}' is not a valid package name` ) } let packageName if (packageSpecifier[0] !== '@') { packageName = packageSpecifier.split('/', 1).join() } else { if (!packageSpecifier.includes('/')) { throw errors.INVALID_MODULE_SPECIFIER( `Module specifier '${packageSpecifier}' is not a valid package name` ) } packageName = packageSpecifier.split('/', 2).join('/') } if (packageName[0] === '.' || packageName.includes('\\') || packageName.includes('%')) { throw errors.INVALID_MODULE_SPECIFIER( `Module specifier '${packageSpecifier}' is not a valid package name` ) } let status status = yield* exports.builtinTarget(packageSpecifier, null, builtins, opts) if (status) return status status = yield* exports.deferred(packageSpecifier, opts) if (status) return status let packageSubpath = '.' + packageSpecifier.substring(packageName.length) status = yield* exports.packageSelf(packageName, packageSubpath, parentURL, opts) if (status) return status parentURL = new URL(parentURL.href) for (const packageURL of exports.lookupPackageRoot(packageName, parentURL)) { const info = yield { package: packageURL } if (info) { if (info.engines) exports.validateEngines(packageURL, info.engines, opts) if (info.exports) { return yield* exports.packageExports(packageURL, packageSubpath, info.exports, opts) } if (packageSubpath === '.') { if (typeof info.main === 'string' && info.main !== '') { packageSubpath = info.main } else { return yield* exports.file('index', packageURL, true, opts) } } status = yield* exports.file(packageSubpath, packageURL, false, opts) if (status === RESOLVED) return status return yield* exports.directory(packageSubpath, packageURL, opts) } } return UNRESOLVED } exports.packageSelf = function* (packageName, packageSubpath, parentURL, opts = {}) { for (const packageURL of exports.lookupPackageScope(parentURL, opts)) { const info = yield { package: packageURL } if (info) { if (info.name !== packageName) return false if (info.exports) { return yield* exports.packageExports(packageURL, packageSubpath, info.exports, opts) } if (packageSubpath === '.') { if (typeof info.main === 'string' && info.main !== '') { packageSubpath = info.main } else { return yield* exports.file('index', packageURL, true, opts) } } const status = yield* exports.file(packageSubpath, packageURL, false, opts) if (status === RESOLVED) return status return yield* exports.directory(packageSubpath, packageURL, opts) } } return UNRESOLVED } exports.packageExports = function* (packageURL, subpath, packageExports, opts = {}) { if (subpath === '.') { let mainExport if (typeof packageExports === 'string' || Array.isArray(packageExports)) { mainExport = packageExports } else if (typeof packageExports === 'object' && packageExports !== null) { const keys = Object.keys(packageExports) if (keys.some((key) => key.startsWith('.'))) { if ('.' in packageExports) mainExport = packageExports['.'] } else { mainExport = packageExports } } if (mainExport) { const status = yield* exports.packageTarget(packageURL, mainExport, null, false, opts) if (status) return status } } else if (typeof packageExports === 'object' && packageExports !== null) { const keys = Object.keys(packageExports) if (keys.every((key) => key.startsWith('.'))) { const status = yield* exports.packageImportsExports( subpath, packageExports, packageURL, false, opts ) if (status) return status } } throw errors.PACKAGE_PATH_NOT_EXPORTED( `Package subpath '${subpath}' is not defined by "exports" in '${packageURL}'` ) } exports.packageImports = function* (specifier, parentURL, opts = {}) { const { imports = null } = opts if (specifier === '#' || specifier.startsWith('#/')) { throw errors.INVALID_MODULE_SPECIFIER( `Module specifier '${specifier}' is not a valid internal imports specifier` ) } for (const packageURL of exports.lookupPackageScope(parentURL, opts)) { const info = yield { package: packageURL } if (info) { if (info.imports) { const status = yield* exports.packageImportsExports( specifier, info.imports, packageURL, true, opts ) if (status) return status } if (specifier.startsWith('#')) { throw errors.PACKAGE_IMPORT_NOT_DEFINED( `Package import specifier '${specifier}' is not defined by "imports" in '${packageURL}'` ) } break } } if (imports) { const status = yield* exports.packageImportsExports(specifier, imports, parentURL, true, opts) if (status) return status } return UNRESOLVED } exports.packageImportsExports = function* ( matchKey, matchObject, packageURL, isImports, opts = {} ) { if (matchKey in matchObject && !matchKey.includes('*')) { const target = matchObject[matchKey] return yield* exports.packageTarget(packageURL, target, null, isImports, opts) } const expansionKeys = Object.keys(matchObject) .filter((key) => key.includes('*')) .sort(exports.patternKeyCompare) for (const expansionKey of expansionKeys) { const patternIndex = expansionKey.indexOf('*') const patternBase = expansionKey.substring(0, patternIndex) if (matchKey.startsWith(patternBase) && matchKey !== patternBase) { const patternTrailer = expansionKey.substring(patternIndex + 1) if ( patternTrailer === '' || (matchKey.endsWith(patternTrailer) && matchKey.length >= expansionKey.length) ) { const target = matchObject[expansionKey] const patternMatch = matchKey.substring( patternBase.length, matchKey.length - patternTrailer.length ) return yield* exports.packageTarget(packageURL, target, patternMatch, isImports, opts) } } } return UNRESOLVED } exports.validateEngines = function validateEngines(packageURL, packageEngines, opts = {}) { const { engines = {} } = opts for (const [engine, range] of Object.entries(packageEngines)) { if (engine in engines) { const version = engines[engine] if (!satisfies(version, range)) { throw errors.UNSUPPORTED_ENGINE( `Package not compatible with engine '${engine}' ${version}, requires range '${range}' defined by "engines" in '${packageURL}'` ) } } } } exports.patternKeyCompare = function patternKeyCompare(keyA, keyB) { const patternIndexA = keyA.indexOf('*') const patternIndexB = keyB.indexOf('*') const baseLengthA = patternIndexA === -1 ? keyA.length : patternIndexA + 1 const baseLengthB = patternIndexB === -1 ? keyB.length : patternIndexB + 1 if (baseLengthA > baseLengthB) return -1 if (baseLengthB > baseLengthA) return 1 if (patternIndexA === -1) return 1 if (patternIndexB === -1) return -1 if (keyA.length > keyB.length) return -1 if (keyB.length > keyA.length) return 1 return 0 } exports.packageTarget = function* (packageURL, target, patternMatch, isImports, opts = {}) { const { conditions = [], matchedConditions = [] } = opts if (typeof target === 'string') { if (!target.startsWith('./') && !isImports) { throw errors.INVALID_PACKAGE_TARGET( `Invalid target '${target}' defined by "exports" in '${packageURL}'` ) } if (patternMatch !== null) { target = target.replaceAll('*', patternMatch) } const status = yield* exports.url(target, packageURL, opts) if (status) return status if ( target === '.' || target === '..' || target[0] === '/' || target.startsWith('./') || target.startsWith('../') ) { const resolved = yield { resolution: new URL(target, packageURL) } return resolved ? RESOLVED : YIELDED } return yield* exports.package(target, packageURL, opts) } if (Array.isArray(target)) { for (const targetValue of target) { const status = yield* exports.packageTarget( packageURL, targetValue, patternMatch, isImports, opts ) if (status) return status } } else if (typeof target === 'object' && target !== null) { let status = UNRESOLVED for (const [condition, targetValue, subset] of exports.conditionMatches( target, conditions, opts )) { matchedConditions.push(condition) status |= yield* exports.packageTarget(packageURL, targetValue, patternMatch, isImports, { ...opts, conditions: subset }) matchedConditions.pop() } if (status) return status } return UNRESOLVED } exports.builtinTarget = function* (packageSpecifier, packageVersion, target, opts = {}) { const { builtinProtocol = 'builtin:', conditions = [], matchedConditions = [] } = opts if (typeof target === 'string') { const targetParts = target.split('@') let targetName let targetVersion if (target[0] !== '@') { targetName = targetParts[0] targetVersion = targetParts[1] || null } else { targetName = targetParts.slice(0, 2).join('@') targetVersion = targetParts[2] || null } if (packageSpecifier === targetName) { if (packageVersion === null && targetVersion === null) { const resolved = yield { resolution: new URL(builtinProtocol + packageSpecifier) } return resolved ? RESOLVED : YIELDED } let version = null if (packageVersion === null) { version = targetVersion } else if (targetVersion === null || packageVersion === targetVersion) { version = packageVersion } if (version !== null) { const resolved = yield { resolution: new URL(builtinProtocol + packageSpecifier + '@' + version) } return resolved ? RESOLVED : YIELDED } } } else if (Array.isArray(target)) { for (const targetValue of target) { const status = yield* exports.builtinTarget( packageSpecifier, packageVersion, targetValue, opts ) if (status) return status } } else if (typeof target === 'object' && target !== null) { let status = UNRESOLVED for (const [condition, targetValue, subset] of exports.conditionMatches( target, conditions, opts )) { matchedConditions.push(condition) status |= yield* exports.builtinTarget(packageSpecifier, packageVersion, targetValue, { ...opts, conditions: subset }) matchedConditions.pop() } if (status) return status } return UNRESOLVED } exports.conditionMatches = function* conditionMatches(target, conditions, opts = {}) { if (conditions.every((condition) => typeof condition === 'string')) { const keys = Object.keys(target) for (const condition of keys) { if (condition === 'default' || conditions.includes(condition)) { yield [condition, target[condition], conditions] return true } } return false } let yielded = false for (const subset of conditions) { if (yield* conditionMatches(target, subset, opts)) { yielded = true } } return yielded } exports.lookupPackageRoot = function* (packageName, parentURL) { parentURL = new URL(parentURL.href) do { const packageURL = new URL('node_modules/' + packageName + '/', parentURL) const info = yield new URL('package.json', packageURL) if (info) return info parentURL.pathname = parentURL.pathname.substring(0, parentURL.pathname.lastIndexOf('/')) if ( parentURL.pathname.length === 3 && exports.isWindowsDriveLetter(parentURL.pathname.substring(1)) ) { break } } while (parentURL.pathname !== '' && parentURL.pathname !== '/') return null } exports.lookupPackageScope = function* lookupPackageScope(scopeURL, opts = {}) { const { resolutions = null } = opts if (resolutions) { for (const { resolution } of exports.preresolved('#package', resolutions, scopeURL, opts)) { if (resolution) return yield resolution } } scopeURL = new URL(scopeURL.href) do { if (scopeURL.pathname.endsWith('/node_modules')) break const info = yield new URL('package.json', scopeURL) if (info) return info scopeURL.pathname = scopeURL.pathname.substring(0, scopeURL.pathname.lastIndexOf('/')) if ( scopeURL.pathname.length === 3 && exports.isWindowsDriveLetter(scopeURL.pathname.substring(1)) ) { break } } while (scopeURL.pathname !== '' && scopeURL.pathname !== '/') return null } exports.file = function* (filename, parentURL, isIndex, opts = {}) { if ( filename === '.' || filename === '..' || filename[filename.length - 1] === '/' || filename[filename.length - 1] === '\\' ) { return UNRESOLVED } if (parentURL.protocol === 'file:' && /%2f|%5c/i.test(filename)) { throw errors.INVALID_MODULE_SPECIFIER(`Module specifier '${filename}' is invalid`) } const { extensions = [] } = opts let status = UNRESOLVED if (!isIndex) { if (yield { resolution: new URL(filename, parentURL) }) { return RESOLVED } status = YIELDED } for (const ext of extensions) { if (filename.endsWith(ext)) continue if (yield { resolution: new URL(filename + ext, parentURL) }) { return RESOLVED } status = YIELDED } return status } exports.directory = function* (dirname, parentURL, opts = {}) { let directoryURL if (dirname[dirname.length - 1] === '/' || dirname[dirname.length - 1] === '\\') { directoryURL = new URL(dirname, parentURL) } else { directoryURL = new URL(dirname + '/', parentURL) } const info = yield { package: new URL('package.json', directoryURL) } if (info) { if (info.exports) { return yield* exports.packageExports(directoryURL, '.', info.exports, opts) } if (typeof info.main === 'string' && info.main !== '') { const status = yield* exports.file(info.main, directoryURL, false, opts) if (status === RESOLVED) return status return yield* exports.directory(info.main, directoryURL, opts) } } return yield* exports.file('index', directoryURL, true, opts) } // https://infra.spec.whatwg.org/#ascii-upper-alpha function isASCIIUpperAlpha(c) { return c >= 0x41 && c <= 0x5a } // https://infra.spec.whatwg.org/#ascii-lower-alpha function isASCIILowerAlpha(c) { return c >= 0x61 && c <= 0x7a } // https://infra.spec.whatwg.org/#ascii-alpha function isASCIIAlpha(c) { return isASCIIUpperAlpha(c) || isASCIILowerAlpha(c) } // https://url.spec.whatwg.org/#windows-drive-letter exports.isWindowsDriveLetter = function isWindowsDriveLetter(input) { return ( input.length >= 2 && isASCIIAlpha(input.charCodeAt(0)) && (input.charCodeAt(1) === 0x3a || input.charCodeAt(1) === 0x7c) ) } // https://url.spec.whatwg.org/#start-with-a-windows-drive-letter exports.startsWithWindowsDriveLetter = function startsWithWindowsDriveLetter(input) { return ( input.length >= 2 && exports.isWindowsDriveLetter(input) && (input.length === 2 || input.charCodeAt(2) === 0x2f || input.charCodeAt(2) === 0x5c || input.charCodeAt(2) === 0x3f || input.charCodeAt(2) === 0x23) ) }