1275 lines
33 KiB
JavaScript
1275 lines
33 KiB
JavaScript
|
// Manually “tree shaken” from:
|
|||
|
// <https://github.com/nodejs/node/blob/45f5c9b/lib/internal/modules/esm/resolve.js>
|
|||
|
// Last checked on: Nov 2, 2023.
|
|||
|
|
|||
|
/**
|
|||
|
* @typedef {import('./errors.js').ErrnoException} ErrnoException
|
|||
|
* @typedef {import('./package-config.js').PackageConfig} PackageConfig
|
|||
|
*/
|
|||
|
|
|||
|
import assert from 'node:assert'
|
|||
|
import {Stats, statSync, realpathSync} from 'node:fs'
|
|||
|
import process from 'node:process'
|
|||
|
import {URL, fileURLToPath, pathToFileURL} from 'node:url'
|
|||
|
import path from 'node:path'
|
|||
|
import {builtinModules} from 'node:module'
|
|||
|
import {defaultGetFormatWithoutErrors} from './get-format.js'
|
|||
|
import {codes} from './errors.js'
|
|||
|
import {getPackageScopeConfig} from './package-config.js'
|
|||
|
import packageJsonReader from './package-json-reader.js'
|
|||
|
import {getConditionsSet} from './utils.js'
|
|||
|
|
|||
|
const RegExpPrototypeSymbolReplace = RegExp.prototype[Symbol.replace]
|
|||
|
|
|||
|
const {
|
|||
|
ERR_NETWORK_IMPORT_DISALLOWED,
|
|||
|
ERR_INVALID_MODULE_SPECIFIER,
|
|||
|
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
|
|||
|
} = codes
|
|||
|
|
|||
|
const own = {}.hasOwnProperty
|
|||
|
|
|||
|
const invalidSegmentRegEx =
|
|||
|
/(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))?(\\|\/|$)/i
|
|||
|
const deprecatedInvalidSegmentRegEx =
|
|||
|
/(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))(\\|\/|$)/i
|
|||
|
const invalidPackageNameRegEx = /^\.|%|\\/
|
|||
|
const patternRegEx = /\*/g
|
|||
|
const encodedSepRegEx = /%2f|%5c/i
|
|||
|
/** @type {Set<string>} */
|
|||
|
const emittedPackageWarnings = new Set()
|
|||
|
|
|||
|
const doubleSlashRegEx = /[/\\]{2}/
|
|||
|
|
|||
|
/**
|
|||
|
*
|
|||
|
* @param {string} target
|
|||
|
* @param {string} request
|
|||
|
* @param {string} match
|
|||
|
* @param {URL} packageJsonUrl
|
|||
|
* @param {boolean} internal
|
|||
|
* @param {URL} base
|
|||
|
* @param {boolean} isTarget
|
|||
|
*/
|
|||
|
function emitInvalidSegmentDeprecation(
|
|||
|
target,
|
|||
|
request,
|
|||
|
match,
|
|||
|
packageJsonUrl,
|
|||
|
internal,
|
|||
|
base,
|
|||
|
isTarget
|
|||
|
) {
|
|||
|
// @ts-expect-error: apparently it does exist, TS.
|
|||
|
if (process.noDeprecation) {
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
const pjsonPath = fileURLToPath(packageJsonUrl)
|
|||
|
const double = doubleSlashRegEx.exec(isTarget ? target : request) !== null
|
|||
|
process.emitWarning(
|
|||
|
`Use of deprecated ${
|
|||
|
double ? 'double slash' : 'leading or trailing slash matching'
|
|||
|
} resolving "${target}" for module ` +
|
|||
|
`request "${request}" ${
|
|||
|
request === match ? '' : `matched to "${match}" `
|
|||
|
}in the "${
|
|||
|
internal ? 'imports' : 'exports'
|
|||
|
}" field module resolution of the package at ${pjsonPath}${
|
|||
|
base ? ` imported from ${fileURLToPath(base)}` : ''
|
|||
|
}.`,
|
|||
|
'DeprecationWarning',
|
|||
|
'DEP0166'
|
|||
|
)
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* @param {URL} url
|
|||
|
* @param {URL} packageJsonUrl
|
|||
|
* @param {URL} base
|
|||
|
* @param {string} [main]
|
|||
|
* @returns {void}
|
|||
|
*/
|
|||
|
function emitLegacyIndexDeprecation(url, packageJsonUrl, base, main) {
|
|||
|
// @ts-expect-error: apparently it does exist, TS.
|
|||
|
if (process.noDeprecation) {
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
const format = defaultGetFormatWithoutErrors(url, {parentURL: base.href})
|
|||
|
if (format !== 'module') return
|
|||
|
const urlPath = fileURLToPath(url.href)
|
|||
|
const pkgPath = fileURLToPath(new URL('.', packageJsonUrl))
|
|||
|
const basePath = fileURLToPath(base)
|
|||
|
if (!main) {
|
|||
|
process.emitWarning(
|
|||
|
`No "main" or "exports" field defined in the package.json for ${pkgPath} resolving the main entry point "${urlPath.slice(
|
|||
|
pkgPath.length
|
|||
|
)}", imported from ${basePath}.\nDefault "index" lookups for the main are deprecated for ES modules.`,
|
|||
|
'DeprecationWarning',
|
|||
|
'DEP0151'
|
|||
|
)
|
|||
|
} else if (path.resolve(pkgPath, main) !== urlPath) {
|
|||
|
process.emitWarning(
|
|||
|
`Package ${pkgPath} has a "main" field set to "${main}", ` +
|
|||
|
`excluding the full filename and extension to the resolved file at "${urlPath.slice(
|
|||
|
pkgPath.length
|
|||
|
)}", imported from ${basePath}.\n Automatic extension resolution of the "main" field is ` +
|
|||
|
'deprecated for ES modules.',
|
|||
|
'DeprecationWarning',
|
|||
|
'DEP0151'
|
|||
|
)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* @param {string} path
|
|||
|
* @returns {Stats}
|
|||
|
*/
|
|||
|
function tryStatSync(path) {
|
|||
|
// Note: from Node 15 onwards we can use `throwIfNoEntry: false` instead.
|
|||
|
try {
|
|||
|
return statSync(path)
|
|||
|
} catch {
|
|||
|
return new Stats()
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Legacy CommonJS main resolution:
|
|||
|
* 1. let M = pkg_url + (json main field)
|
|||
|
* 2. TRY(M, M.js, M.json, M.node)
|
|||
|
* 3. TRY(M/index.js, M/index.json, M/index.node)
|
|||
|
* 4. TRY(pkg_url/index.js, pkg_url/index.json, pkg_url/index.node)
|
|||
|
* 5. NOT_FOUND
|
|||
|
*
|
|||
|
* @param {URL} url
|
|||
|
* @returns {boolean}
|
|||
|
*/
|
|||
|
function fileExists(url) {
|
|||
|
const stats = statSync(url, {throwIfNoEntry: false})
|
|||
|
const isFile = stats ? stats.isFile() : undefined
|
|||
|
return isFile === null || isFile === undefined ? false : isFile
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* @param {URL} packageJsonUrl
|
|||
|
* @param {PackageConfig} packageConfig
|
|||
|
* @param {URL} base
|
|||
|
* @returns {URL}
|
|||
|
*/
|
|||
|
function legacyMainResolve(packageJsonUrl, packageConfig, base) {
|
|||
|
/** @type {URL | undefined} */
|
|||
|
let guess
|
|||
|
if (packageConfig.main !== undefined) {
|
|||
|
guess = new URL(packageConfig.main, packageJsonUrl)
|
|||
|
// Note: fs check redundances will be handled by Descriptor cache here.
|
|||
|
if (fileExists(guess)) return guess
|
|||
|
|
|||
|
const tries = [
|
|||
|
`./${packageConfig.main}.js`,
|
|||
|
`./${packageConfig.main}.json`,
|
|||
|
`./${packageConfig.main}.node`,
|
|||
|
`./${packageConfig.main}/index.js`,
|
|||
|
`./${packageConfig.main}/index.json`,
|
|||
|
`./${packageConfig.main}/index.node`
|
|||
|
]
|
|||
|
let i = -1
|
|||
|
|
|||
|
while (++i < tries.length) {
|
|||
|
guess = new URL(tries[i], packageJsonUrl)
|
|||
|
if (fileExists(guess)) break
|
|||
|
guess = undefined
|
|||
|
}
|
|||
|
|
|||
|
if (guess) {
|
|||
|
emitLegacyIndexDeprecation(
|
|||
|
guess,
|
|||
|
packageJsonUrl,
|
|||
|
base,
|
|||
|
packageConfig.main
|
|||
|
)
|
|||
|
return guess
|
|||
|
}
|
|||
|
// Fallthrough.
|
|||
|
}
|
|||
|
|
|||
|
const tries = ['./index.js', './index.json', './index.node']
|
|||
|
let i = -1
|
|||
|
|
|||
|
while (++i < tries.length) {
|
|||
|
guess = new URL(tries[i], packageJsonUrl)
|
|||
|
if (fileExists(guess)) break
|
|||
|
guess = undefined
|
|||
|
}
|
|||
|
|
|||
|
if (guess) {
|
|||
|
emitLegacyIndexDeprecation(guess, packageJsonUrl, base, packageConfig.main)
|
|||
|
return guess
|
|||
|
}
|
|||
|
|
|||
|
// Not found.
|
|||
|
throw new ERR_MODULE_NOT_FOUND(
|
|||
|
fileURLToPath(new URL('.', packageJsonUrl)),
|
|||
|
fileURLToPath(base)
|
|||
|
)
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* @param {URL} resolved
|
|||
|
* @param {URL} base
|
|||
|
* @param {boolean} [preserveSymlinks]
|
|||
|
* @returns {URL}
|
|||
|
*/
|
|||
|
function finalizeResolution(resolved, base, preserveSymlinks) {
|
|||
|
if (encodedSepRegEx.exec(resolved.pathname) !== null) {
|
|||
|
throw new ERR_INVALID_MODULE_SPECIFIER(
|
|||
|
resolved.pathname,
|
|||
|
'must not include encoded "/" or "\\" characters',
|
|||
|
fileURLToPath(base)
|
|||
|
)
|
|||
|
}
|
|||
|
|
|||
|
/** @type {string} */
|
|||
|
let filePath
|
|||
|
|
|||
|
try {
|
|||
|
filePath = fileURLToPath(resolved)
|
|||
|
} catch (error) {
|
|||
|
const cause = /** @type {ErrnoException} */ (error)
|
|||
|
Object.defineProperty(cause, 'input', {value: String(resolved)})
|
|||
|
Object.defineProperty(cause, 'module', {value: String(base)})
|
|||
|
throw cause
|
|||
|
}
|
|||
|
|
|||
|
const stats = tryStatSync(
|
|||
|
filePath.endsWith('/') ? filePath.slice(-1) : filePath
|
|||
|
)
|
|||
|
|
|||
|
if (stats.isDirectory()) {
|
|||
|
const error = new ERR_UNSUPPORTED_DIR_IMPORT(filePath, fileURLToPath(base))
|
|||
|
// @ts-expect-error Add this for `import.meta.resolve`.
|
|||
|
error.url = String(resolved)
|
|||
|
throw error
|
|||
|
}
|
|||
|
|
|||
|
if (!stats.isFile()) {
|
|||
|
const error = new ERR_MODULE_NOT_FOUND(
|
|||
|
filePath || resolved.pathname,
|
|||
|
base && fileURLToPath(base),
|
|||
|
true
|
|||
|
)
|
|||
|
// @ts-expect-error Add this for `import.meta.resolve`.
|
|||
|
error.url = String(resolved)
|
|||
|
throw error
|
|||
|
}
|
|||
|
|
|||
|
if (!preserveSymlinks) {
|
|||
|
const real = realpathSync(filePath)
|
|||
|
const {search, hash} = resolved
|
|||
|
resolved = pathToFileURL(real + (filePath.endsWith(path.sep) ? '/' : ''))
|
|||
|
resolved.search = search
|
|||
|
resolved.hash = hash
|
|||
|
}
|
|||
|
|
|||
|
return resolved
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* @param {string} specifier
|
|||
|
* @param {URL | undefined} packageJsonUrl
|
|||
|
* @param {URL} base
|
|||
|
* @returns {Error}
|
|||
|
*/
|
|||
|
function importNotDefined(specifier, packageJsonUrl, base) {
|
|||
|
return new ERR_PACKAGE_IMPORT_NOT_DEFINED(
|
|||
|
specifier,
|
|||
|
packageJsonUrl && fileURLToPath(new URL('.', packageJsonUrl)),
|
|||
|
fileURLToPath(base)
|
|||
|
)
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* @param {string} subpath
|
|||
|
* @param {URL} packageJsonUrl
|
|||
|
* @param {URL} base
|
|||
|
* @returns {Error}
|
|||
|
*/
|
|||
|
function exportsNotFound(subpath, packageJsonUrl, base) {
|
|||
|
return new ERR_PACKAGE_PATH_NOT_EXPORTED(
|
|||
|
fileURLToPath(new URL('.', packageJsonUrl)),
|
|||
|
subpath,
|
|||
|
base && fileURLToPath(base)
|
|||
|
)
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* @param {string} request
|
|||
|
* @param {string} match
|
|||
|
* @param {URL} packageJsonUrl
|
|||
|
* @param {boolean} internal
|
|||
|
* @param {URL} [base]
|
|||
|
* @returns {never}
|
|||
|
*/
|
|||
|
function throwInvalidSubpath(request, match, packageJsonUrl, internal, base) {
|
|||
|
const reason = `request is not a valid match in pattern "${match}" for the "${
|
|||
|
internal ? 'imports' : 'exports'
|
|||
|
}" resolution of ${fileURLToPath(packageJsonUrl)}`
|
|||
|
throw new ERR_INVALID_MODULE_SPECIFIER(
|
|||
|
request,
|
|||
|
reason,
|
|||
|
base && fileURLToPath(base)
|
|||
|
)
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* @param {string} subpath
|
|||
|
* @param {unknown} target
|
|||
|
* @param {URL} packageJsonUrl
|
|||
|
* @param {boolean} internal
|
|||
|
* @param {URL} [base]
|
|||
|
* @returns {Error}
|
|||
|
*/
|
|||
|
function invalidPackageTarget(subpath, target, packageJsonUrl, internal, base) {
|
|||
|
target =
|
|||
|
typeof target === 'object' && target !== null
|
|||
|
? JSON.stringify(target, null, '')
|
|||
|
: `${target}`
|
|||
|
|
|||
|
return new ERR_INVALID_PACKAGE_TARGET(
|
|||
|
fileURLToPath(new URL('.', packageJsonUrl)),
|
|||
|
subpath,
|
|||
|
target,
|
|||
|
internal,
|
|||
|
base && fileURLToPath(base)
|
|||
|
)
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* @param {string} target
|
|||
|
* @param {string} subpath
|
|||
|
* @param {string} match
|
|||
|
* @param {URL} packageJsonUrl
|
|||
|
* @param {URL} base
|
|||
|
* @param {boolean} pattern
|
|||
|
* @param {boolean} internal
|
|||
|
* @param {boolean} isPathMap
|
|||
|
* @param {Set<string> | undefined} conditions
|
|||
|
* @returns {URL}
|
|||
|
*/
|
|||
|
function resolvePackageTargetString(
|
|||
|
target,
|
|||
|
subpath,
|
|||
|
match,
|
|||
|
packageJsonUrl,
|
|||
|
base,
|
|||
|
pattern,
|
|||
|
internal,
|
|||
|
isPathMap,
|
|||
|
conditions
|
|||
|
) {
|
|||
|
if (subpath !== '' && !pattern && target[target.length - 1] !== '/')
|
|||
|
throw invalidPackageTarget(match, target, packageJsonUrl, internal, base)
|
|||
|
|
|||
|
if (!target.startsWith('./')) {
|
|||
|
if (internal && !target.startsWith('../') && !target.startsWith('/')) {
|
|||
|
let isURL = false
|
|||
|
|
|||
|
try {
|
|||
|
new URL(target)
|
|||
|
isURL = true
|
|||
|
} catch {
|
|||
|
// Continue regardless of error.
|
|||
|
}
|
|||
|
|
|||
|
if (!isURL) {
|
|||
|
const exportTarget = pattern
|
|||
|
? RegExpPrototypeSymbolReplace.call(
|
|||
|
patternRegEx,
|
|||
|
target,
|
|||
|
() => subpath
|
|||
|
)
|
|||
|
: target + subpath
|
|||
|
|
|||
|
return packageResolve(exportTarget, packageJsonUrl, conditions)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
throw invalidPackageTarget(match, target, packageJsonUrl, internal, base)
|
|||
|
}
|
|||
|
|
|||
|
if (invalidSegmentRegEx.exec(target.slice(2)) !== null) {
|
|||
|
if (deprecatedInvalidSegmentRegEx.exec(target.slice(2)) === null) {
|
|||
|
if (!isPathMap) {
|
|||
|
const request = pattern
|
|||
|
? match.replace('*', () => subpath)
|
|||
|
: match + subpath
|
|||
|
const resolvedTarget = pattern
|
|||
|
? RegExpPrototypeSymbolReplace.call(
|
|||
|
patternRegEx,
|
|||
|
target,
|
|||
|
() => subpath
|
|||
|
)
|
|||
|
: target
|
|||
|
emitInvalidSegmentDeprecation(
|
|||
|
resolvedTarget,
|
|||
|
request,
|
|||
|
match,
|
|||
|
packageJsonUrl,
|
|||
|
internal,
|
|||
|
base,
|
|||
|
true
|
|||
|
)
|
|||
|
}
|
|||
|
} else {
|
|||
|
throw invalidPackageTarget(match, target, packageJsonUrl, internal, base)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const resolved = new URL(target, packageJsonUrl)
|
|||
|
const resolvedPath = resolved.pathname
|
|||
|
const packagePath = new URL('.', packageJsonUrl).pathname
|
|||
|
|
|||
|
if (!resolvedPath.startsWith(packagePath))
|
|||
|
throw invalidPackageTarget(match, target, packageJsonUrl, internal, base)
|
|||
|
|
|||
|
if (subpath === '') return resolved
|
|||
|
|
|||
|
if (invalidSegmentRegEx.exec(subpath) !== null) {
|
|||
|
const request = pattern
|
|||
|
? match.replace('*', () => subpath)
|
|||
|
: match + subpath
|
|||
|
if (deprecatedInvalidSegmentRegEx.exec(subpath) === null) {
|
|||
|
if (!isPathMap) {
|
|||
|
const resolvedTarget = pattern
|
|||
|
? RegExpPrototypeSymbolReplace.call(
|
|||
|
patternRegEx,
|
|||
|
target,
|
|||
|
() => subpath
|
|||
|
)
|
|||
|
: target
|
|||
|
emitInvalidSegmentDeprecation(
|
|||
|
resolvedTarget,
|
|||
|
request,
|
|||
|
match,
|
|||
|
packageJsonUrl,
|
|||
|
internal,
|
|||
|
base,
|
|||
|
false
|
|||
|
)
|
|||
|
}
|
|||
|
} else {
|
|||
|
throwInvalidSubpath(request, match, packageJsonUrl, internal, base)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if (pattern) {
|
|||
|
return new URL(
|
|||
|
RegExpPrototypeSymbolReplace.call(
|
|||
|
patternRegEx,
|
|||
|
resolved.href,
|
|||
|
() => subpath
|
|||
|
)
|
|||
|
)
|
|||
|
}
|
|||
|
|
|||
|
return new URL(subpath, resolved)
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* @param {string} key
|
|||
|
* @returns {boolean}
|
|||
|
*/
|
|||
|
function isArrayIndex(key) {
|
|||
|
const keyNumber = Number(key)
|
|||
|
if (`${keyNumber}` !== key) return false
|
|||
|
return keyNumber >= 0 && keyNumber < 0xff_ff_ff_ff
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* @param {URL} packageJsonUrl
|
|||
|
* @param {unknown} target
|
|||
|
* @param {string} subpath
|
|||
|
* @param {string} packageSubpath
|
|||
|
* @param {URL} base
|
|||
|
* @param {boolean} pattern
|
|||
|
* @param {boolean} internal
|
|||
|
* @param {boolean} isPathMap
|
|||
|
* @param {Set<string> | undefined} conditions
|
|||
|
* @returns {URL | null}
|
|||
|
*/
|
|||
|
function resolvePackageTarget(
|
|||
|
packageJsonUrl,
|
|||
|
target,
|
|||
|
subpath,
|
|||
|
packageSubpath,
|
|||
|
base,
|
|||
|
pattern,
|
|||
|
internal,
|
|||
|
isPathMap,
|
|||
|
conditions
|
|||
|
) {
|
|||
|
if (typeof target === 'string') {
|
|||
|
return resolvePackageTargetString(
|
|||
|
target,
|
|||
|
subpath,
|
|||
|
packageSubpath,
|
|||
|
packageJsonUrl,
|
|||
|
base,
|
|||
|
pattern,
|
|||
|
internal,
|
|||
|
isPathMap,
|
|||
|
conditions
|
|||
|
)
|
|||
|
}
|
|||
|
|
|||
|
if (Array.isArray(target)) {
|
|||
|
/** @type {Array<unknown>} */
|
|||
|
const targetList = target
|
|||
|
if (targetList.length === 0) return null
|
|||
|
|
|||
|
/** @type {ErrnoException | null | undefined} */
|
|||
|
let lastException
|
|||
|
let i = -1
|
|||
|
|
|||
|
while (++i < targetList.length) {
|
|||
|
const targetItem = targetList[i]
|
|||
|
/** @type {URL | null} */
|
|||
|
let resolveResult
|
|||
|
try {
|
|||
|
resolveResult = resolvePackageTarget(
|
|||
|
packageJsonUrl,
|
|||
|
targetItem,
|
|||
|
subpath,
|
|||
|
packageSubpath,
|
|||
|
base,
|
|||
|
pattern,
|
|||
|
internal,
|
|||
|
isPathMap,
|
|||
|
conditions
|
|||
|
)
|
|||
|
} catch (error) {
|
|||
|
const exception = /** @type {ErrnoException} */ (error)
|
|||
|
lastException = exception
|
|||
|
if (exception.code === 'ERR_INVALID_PACKAGE_TARGET') continue
|
|||
|
throw error
|
|||
|
}
|
|||
|
|
|||
|
if (resolveResult === undefined) continue
|
|||
|
|
|||
|
if (resolveResult === null) {
|
|||
|
lastException = null
|
|||
|
continue
|
|||
|
}
|
|||
|
|
|||
|
return resolveResult
|
|||
|
}
|
|||
|
|
|||
|
if (lastException === undefined || lastException === null) {
|
|||
|
return null
|
|||
|
}
|
|||
|
|
|||
|
throw lastException
|
|||
|
}
|
|||
|
|
|||
|
if (typeof target === 'object' && target !== null) {
|
|||
|
const keys = Object.getOwnPropertyNames(target)
|
|||
|
let i = -1
|
|||
|
|
|||
|
while (++i < keys.length) {
|
|||
|
const key = keys[i]
|
|||
|
if (isArrayIndex(key)) {
|
|||
|
throw new ERR_INVALID_PACKAGE_CONFIG(
|
|||
|
fileURLToPath(packageJsonUrl),
|
|||
|
base,
|
|||
|
'"exports" cannot contain numeric property keys.'
|
|||
|
)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
i = -1
|
|||
|
|
|||
|
while (++i < keys.length) {
|
|||
|
const key = keys[i]
|
|||
|
if (key === 'default' || (conditions && conditions.has(key))) {
|
|||
|
// @ts-expect-error: indexable.
|
|||
|
const conditionalTarget = /** @type {unknown} */ (target[key])
|
|||
|
const resolveResult = resolvePackageTarget(
|
|||
|
packageJsonUrl,
|
|||
|
conditionalTarget,
|
|||
|
subpath,
|
|||
|
packageSubpath,
|
|||
|
base,
|
|||
|
pattern,
|
|||
|
internal,
|
|||
|
isPathMap,
|
|||
|
conditions
|
|||
|
)
|
|||
|
if (resolveResult === undefined) continue
|
|||
|
return resolveResult
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return null
|
|||
|
}
|
|||
|
|
|||
|
if (target === null) {
|
|||
|
return null
|
|||
|
}
|
|||
|
|
|||
|
throw invalidPackageTarget(
|
|||
|
packageSubpath,
|
|||
|
target,
|
|||
|
packageJsonUrl,
|
|||
|
internal,
|
|||
|
base
|
|||
|
)
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* @param {unknown} exports
|
|||
|
* @param {URL} packageJsonUrl
|
|||
|
* @param {URL} base
|
|||
|
* @returns {boolean}
|
|||
|
*/
|
|||
|
function isConditionalExportsMainSugar(exports, packageJsonUrl, base) {
|
|||
|
if (typeof exports === 'string' || Array.isArray(exports)) return true
|
|||
|
if (typeof exports !== 'object' || exports === null) return false
|
|||
|
|
|||
|
const keys = Object.getOwnPropertyNames(exports)
|
|||
|
let isConditionalSugar = false
|
|||
|
let i = 0
|
|||
|
let j = -1
|
|||
|
while (++j < keys.length) {
|
|||
|
const key = keys[j]
|
|||
|
const curIsConditionalSugar = key === '' || key[0] !== '.'
|
|||
|
if (i++ === 0) {
|
|||
|
isConditionalSugar = curIsConditionalSugar
|
|||
|
} else if (isConditionalSugar !== curIsConditionalSugar) {
|
|||
|
throw new ERR_INVALID_PACKAGE_CONFIG(
|
|||
|
fileURLToPath(packageJsonUrl),
|
|||
|
base,
|
|||
|
'"exports" cannot contain some keys starting with \'.\' and some not.' +
|
|||
|
' The exports object must either be an object of package subpath keys' +
|
|||
|
' or an object of main entry condition name keys only.'
|
|||
|
)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return isConditionalSugar
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* @param {string} match
|
|||
|
* @param {URL} pjsonUrl
|
|||
|
* @param {URL} base
|
|||
|
*/
|
|||
|
function emitTrailingSlashPatternDeprecation(match, pjsonUrl, base) {
|
|||
|
// @ts-expect-error: apparently it does exist, TS.
|
|||
|
if (process.noDeprecation) {
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
const pjsonPath = fileURLToPath(pjsonUrl)
|
|||
|
if (emittedPackageWarnings.has(pjsonPath + '|' + match)) return
|
|||
|
emittedPackageWarnings.add(pjsonPath + '|' + match)
|
|||
|
process.emitWarning(
|
|||
|
`Use of deprecated trailing slash pattern mapping "${match}" in the ` +
|
|||
|
`"exports" field module resolution of the package at ${pjsonPath}${
|
|||
|
base ? ` imported from ${fileURLToPath(base)}` : ''
|
|||
|
}. Mapping specifiers ending in "/" is no longer supported.`,
|
|||
|
'DeprecationWarning',
|
|||
|
'DEP0155'
|
|||
|
)
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* @param {URL} packageJsonUrl
|
|||
|
* @param {string} packageSubpath
|
|||
|
* @param {Record<string, unknown>} packageConfig
|
|||
|
* @param {URL} base
|
|||
|
* @param {Set<string> | undefined} conditions
|
|||
|
* @returns {URL}
|
|||
|
*/
|
|||
|
function packageExportsResolve(
|
|||
|
packageJsonUrl,
|
|||
|
packageSubpath,
|
|||
|
packageConfig,
|
|||
|
base,
|
|||
|
conditions
|
|||
|
) {
|
|||
|
let exports = packageConfig.exports
|
|||
|
|
|||
|
if (isConditionalExportsMainSugar(exports, packageJsonUrl, base)) {
|
|||
|
exports = {'.': exports}
|
|||
|
}
|
|||
|
|
|||
|
if (
|
|||
|
own.call(exports, packageSubpath) &&
|
|||
|
!packageSubpath.includes('*') &&
|
|||
|
!packageSubpath.endsWith('/')
|
|||
|
) {
|
|||
|
// @ts-expect-error: indexable.
|
|||
|
const target = exports[packageSubpath]
|
|||
|
const resolveResult = resolvePackageTarget(
|
|||
|
packageJsonUrl,
|
|||
|
target,
|
|||
|
'',
|
|||
|
packageSubpath,
|
|||
|
base,
|
|||
|
false,
|
|||
|
false,
|
|||
|
false,
|
|||
|
conditions
|
|||
|
)
|
|||
|
if (resolveResult === null || resolveResult === undefined) {
|
|||
|
throw exportsNotFound(packageSubpath, packageJsonUrl, base)
|
|||
|
}
|
|||
|
|
|||
|
return resolveResult
|
|||
|
}
|
|||
|
|
|||
|
let bestMatch = ''
|
|||
|
let bestMatchSubpath = ''
|
|||
|
const keys = Object.getOwnPropertyNames(exports)
|
|||
|
let i = -1
|
|||
|
|
|||
|
while (++i < keys.length) {
|
|||
|
const key = keys[i]
|
|||
|
const patternIndex = key.indexOf('*')
|
|||
|
|
|||
|
if (
|
|||
|
patternIndex !== -1 &&
|
|||
|
packageSubpath.startsWith(key.slice(0, patternIndex))
|
|||
|
) {
|
|||
|
// When this reaches EOL, this can throw at the top of the whole function:
|
|||
|
//
|
|||
|
// if (StringPrototypeEndsWith(packageSubpath, '/'))
|
|||
|
// throwInvalidSubpath(packageSubpath)
|
|||
|
//
|
|||
|
// To match "imports" and the spec.
|
|||
|
if (packageSubpath.endsWith('/')) {
|
|||
|
emitTrailingSlashPatternDeprecation(
|
|||
|
packageSubpath,
|
|||
|
packageJsonUrl,
|
|||
|
base
|
|||
|
)
|
|||
|
}
|
|||
|
|
|||
|
const patternTrailer = key.slice(patternIndex + 1)
|
|||
|
|
|||
|
if (
|
|||
|
packageSubpath.length >= key.length &&
|
|||
|
packageSubpath.endsWith(patternTrailer) &&
|
|||
|
patternKeyCompare(bestMatch, key) === 1 &&
|
|||
|
key.lastIndexOf('*') === patternIndex
|
|||
|
) {
|
|||
|
bestMatch = key
|
|||
|
bestMatchSubpath = packageSubpath.slice(
|
|||
|
patternIndex,
|
|||
|
packageSubpath.length - patternTrailer.length
|
|||
|
)
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if (bestMatch) {
|
|||
|
// @ts-expect-error: indexable.
|
|||
|
const target = /** @type {unknown} */ (exports[bestMatch])
|
|||
|
const resolveResult = resolvePackageTarget(
|
|||
|
packageJsonUrl,
|
|||
|
target,
|
|||
|
bestMatchSubpath,
|
|||
|
bestMatch,
|
|||
|
base,
|
|||
|
true,
|
|||
|
false,
|
|||
|
packageSubpath.endsWith('/'),
|
|||
|
conditions
|
|||
|
)
|
|||
|
|
|||
|
if (resolveResult === null || resolveResult === undefined) {
|
|||
|
throw exportsNotFound(packageSubpath, packageJsonUrl, base)
|
|||
|
}
|
|||
|
|
|||
|
return resolveResult
|
|||
|
}
|
|||
|
|
|||
|
throw exportsNotFound(packageSubpath, packageJsonUrl, base)
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* @param {string} a
|
|||
|
* @param {string} b
|
|||
|
*/
|
|||
|
function patternKeyCompare(a, b) {
|
|||
|
const aPatternIndex = a.indexOf('*')
|
|||
|
const bPatternIndex = b.indexOf('*')
|
|||
|
const baseLengthA = aPatternIndex === -1 ? a.length : aPatternIndex + 1
|
|||
|
const baseLengthB = bPatternIndex === -1 ? b.length : bPatternIndex + 1
|
|||
|
if (baseLengthA > baseLengthB) return -1
|
|||
|
if (baseLengthB > baseLengthA) return 1
|
|||
|
if (aPatternIndex === -1) return 1
|
|||
|
if (bPatternIndex === -1) return -1
|
|||
|
if (a.length > b.length) return -1
|
|||
|
if (b.length > a.length) return 1
|
|||
|
return 0
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* @param {string} name
|
|||
|
* @param {URL} base
|
|||
|
* @param {Set<string>} [conditions]
|
|||
|
* @returns {URL}
|
|||
|
*/
|
|||
|
function packageImportsResolve(name, base, conditions) {
|
|||
|
if (name === '#' || name.startsWith('#/') || name.endsWith('/')) {
|
|||
|
const reason = 'is not a valid internal imports specifier name'
|
|||
|
throw new ERR_INVALID_MODULE_SPECIFIER(name, reason, fileURLToPath(base))
|
|||
|
}
|
|||
|
|
|||
|
/** @type {URL | undefined} */
|
|||
|
let packageJsonUrl
|
|||
|
|
|||
|
const packageConfig = getPackageScopeConfig(base)
|
|||
|
|
|||
|
if (packageConfig.exists) {
|
|||
|
packageJsonUrl = pathToFileURL(packageConfig.pjsonPath)
|
|||
|
const imports = packageConfig.imports
|
|||
|
if (imports) {
|
|||
|
if (own.call(imports, name) && !name.includes('*')) {
|
|||
|
const resolveResult = resolvePackageTarget(
|
|||
|
packageJsonUrl,
|
|||
|
imports[name],
|
|||
|
'',
|
|||
|
name,
|
|||
|
base,
|
|||
|
false,
|
|||
|
true,
|
|||
|
false,
|
|||
|
conditions
|
|||
|
)
|
|||
|
if (resolveResult !== null && resolveResult !== undefined) {
|
|||
|
return resolveResult
|
|||
|
}
|
|||
|
} else {
|
|||
|
let bestMatch = ''
|
|||
|
let bestMatchSubpath = ''
|
|||
|
const keys = Object.getOwnPropertyNames(imports)
|
|||
|
let i = -1
|
|||
|
|
|||
|
while (++i < keys.length) {
|
|||
|
const key = keys[i]
|
|||
|
const patternIndex = key.indexOf('*')
|
|||
|
|
|||
|
if (patternIndex !== -1 && name.startsWith(key.slice(0, -1))) {
|
|||
|
const patternTrailer = key.slice(patternIndex + 1)
|
|||
|
if (
|
|||
|
name.length >= key.length &&
|
|||
|
name.endsWith(patternTrailer) &&
|
|||
|
patternKeyCompare(bestMatch, key) === 1 &&
|
|||
|
key.lastIndexOf('*') === patternIndex
|
|||
|
) {
|
|||
|
bestMatch = key
|
|||
|
bestMatchSubpath = name.slice(
|
|||
|
patternIndex,
|
|||
|
name.length - patternTrailer.length
|
|||
|
)
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if (bestMatch) {
|
|||
|
const target = imports[bestMatch]
|
|||
|
const resolveResult = resolvePackageTarget(
|
|||
|
packageJsonUrl,
|
|||
|
target,
|
|||
|
bestMatchSubpath,
|
|||
|
bestMatch,
|
|||
|
base,
|
|||
|
true,
|
|||
|
true,
|
|||
|
false,
|
|||
|
conditions
|
|||
|
)
|
|||
|
|
|||
|
if (resolveResult !== null && resolveResult !== undefined) {
|
|||
|
return resolveResult
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
throw importNotDefined(name, packageJsonUrl, base)
|
|||
|
}
|
|||
|
|
|||
|
// Note: In Node.js, `getPackageType` is here.
|
|||
|
// To prevent a circular dependency, we move it to
|
|||
|
// `resolve-get-package-type.js`.
|
|||
|
|
|||
|
/**
|
|||
|
* @param {string} specifier
|
|||
|
* @param {URL} base
|
|||
|
*/
|
|||
|
function parsePackageName(specifier, base) {
|
|||
|
let separatorIndex = specifier.indexOf('/')
|
|||
|
let validPackageName = true
|
|||
|
let isScoped = false
|
|||
|
if (specifier[0] === '@') {
|
|||
|
isScoped = true
|
|||
|
if (separatorIndex === -1 || specifier.length === 0) {
|
|||
|
validPackageName = false
|
|||
|
} else {
|
|||
|
separatorIndex = specifier.indexOf('/', separatorIndex + 1)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const packageName =
|
|||
|
separatorIndex === -1 ? specifier : specifier.slice(0, separatorIndex)
|
|||
|
|
|||
|
// Package name cannot have leading . and cannot have percent-encoding or
|
|||
|
// \\ separators.
|
|||
|
if (invalidPackageNameRegEx.exec(packageName) !== null) {
|
|||
|
validPackageName = false
|
|||
|
}
|
|||
|
|
|||
|
if (!validPackageName) {
|
|||
|
throw new ERR_INVALID_MODULE_SPECIFIER(
|
|||
|
specifier,
|
|||
|
'is not a valid package name',
|
|||
|
fileURLToPath(base)
|
|||
|
)
|
|||
|
}
|
|||
|
|
|||
|
const packageSubpath =
|
|||
|
'.' + (separatorIndex === -1 ? '' : specifier.slice(separatorIndex))
|
|||
|
|
|||
|
return {packageName, packageSubpath, isScoped}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* @param {string} specifier
|
|||
|
* @param {URL} base
|
|||
|
* @param {Set<string> | undefined} conditions
|
|||
|
* @returns {URL}
|
|||
|
*/
|
|||
|
function packageResolve(specifier, base, conditions) {
|
|||
|
if (builtinModules.includes(specifier)) {
|
|||
|
return new URL('node:' + specifier)
|
|||
|
}
|
|||
|
|
|||
|
const {packageName, packageSubpath, isScoped} = parsePackageName(
|
|||
|
specifier,
|
|||
|
base
|
|||
|
)
|
|||
|
|
|||
|
// ResolveSelf
|
|||
|
const packageConfig = getPackageScopeConfig(base)
|
|||
|
|
|||
|
// Can’t test.
|
|||
|
/* c8 ignore next 16 */
|
|||
|
if (packageConfig.exists) {
|
|||
|
const packageJsonUrl = pathToFileURL(packageConfig.pjsonPath)
|
|||
|
if (
|
|||
|
packageConfig.name === packageName &&
|
|||
|
packageConfig.exports !== undefined &&
|
|||
|
packageConfig.exports !== null
|
|||
|
) {
|
|||
|
return packageExportsResolve(
|
|||
|
packageJsonUrl,
|
|||
|
packageSubpath,
|
|||
|
packageConfig,
|
|||
|
base,
|
|||
|
conditions
|
|||
|
)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
let packageJsonUrl = new URL(
|
|||
|
'./node_modules/' + packageName + '/package.json',
|
|||
|
base
|
|||
|
)
|
|||
|
let packageJsonPath = fileURLToPath(packageJsonUrl)
|
|||
|
/** @type {string} */
|
|||
|
let lastPath
|
|||
|
do {
|
|||
|
const stat = tryStatSync(packageJsonPath.slice(0, -13))
|
|||
|
if (!stat.isDirectory()) {
|
|||
|
lastPath = packageJsonPath
|
|||
|
packageJsonUrl = new URL(
|
|||
|
(isScoped ? '../../../../node_modules/' : '../../../node_modules/') +
|
|||
|
packageName +
|
|||
|
'/package.json',
|
|||
|
packageJsonUrl
|
|||
|
)
|
|||
|
packageJsonPath = fileURLToPath(packageJsonUrl)
|
|||
|
continue
|
|||
|
}
|
|||
|
|
|||
|
// Package match.
|
|||
|
const packageConfig = packageJsonReader.read(packageJsonPath, {
|
|||
|
base,
|
|||
|
specifier
|
|||
|
})
|
|||
|
if (packageConfig.exports !== undefined && packageConfig.exports !== null) {
|
|||
|
return packageExportsResolve(
|
|||
|
packageJsonUrl,
|
|||
|
packageSubpath,
|
|||
|
packageConfig,
|
|||
|
base,
|
|||
|
conditions
|
|||
|
)
|
|||
|
}
|
|||
|
|
|||
|
if (packageSubpath === '.') {
|
|||
|
return legacyMainResolve(packageJsonUrl, packageConfig, base)
|
|||
|
}
|
|||
|
|
|||
|
return new URL(packageSubpath, packageJsonUrl)
|
|||
|
// Cross-platform root check.
|
|||
|
} while (packageJsonPath.length !== lastPath.length)
|
|||
|
|
|||
|
throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base), false)
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* @param {string} specifier
|
|||
|
* @returns {boolean}
|
|||
|
*/
|
|||
|
function isRelativeSpecifier(specifier) {
|
|||
|
if (specifier[0] === '.') {
|
|||
|
if (specifier.length === 1 || specifier[1] === '/') return true
|
|||
|
if (
|
|||
|
specifier[1] === '.' &&
|
|||
|
(specifier.length === 2 || specifier[2] === '/')
|
|||
|
) {
|
|||
|
return true
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return false
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* @param {string} specifier
|
|||
|
* @returns {boolean}
|
|||
|
*/
|
|||
|
function shouldBeTreatedAsRelativeOrAbsolutePath(specifier) {
|
|||
|
if (specifier === '') return false
|
|||
|
if (specifier[0] === '/') return true
|
|||
|
return isRelativeSpecifier(specifier)
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* The “Resolver Algorithm Specification” as detailed in the Node docs (which is
|
|||
|
* sync and slightly lower-level than `resolve`).
|
|||
|
*
|
|||
|
* @param {string} specifier
|
|||
|
* `/example.js`, `./example.js`, `../example.js`, `some-package`, `fs`, etc.
|
|||
|
* @param {URL} base
|
|||
|
* Full URL (to a file) that `specifier` is resolved relative from.
|
|||
|
* @param {Set<string>} [conditions]
|
|||
|
* Conditions.
|
|||
|
* @param {boolean} [preserveSymlinks]
|
|||
|
* Keep symlinks instead of resolving them.
|
|||
|
* @returns {URL}
|
|||
|
* A URL object to the found thing.
|
|||
|
*/
|
|||
|
export function moduleResolve(specifier, base, conditions, preserveSymlinks) {
|
|||
|
const protocol = base.protocol
|
|||
|
const isRemote = protocol === 'http:' || protocol === 'https:'
|
|||
|
// Order swapped from spec for minor perf gain.
|
|||
|
// Ok since relative URLs cannot parse as URLs.
|
|||
|
/** @type {URL | undefined} */
|
|||
|
let resolved
|
|||
|
|
|||
|
if (shouldBeTreatedAsRelativeOrAbsolutePath(specifier)) {
|
|||
|
resolved = new URL(specifier, base)
|
|||
|
} else if (!isRemote && specifier[0] === '#') {
|
|||
|
resolved = packageImportsResolve(specifier, base, conditions)
|
|||
|
} else {
|
|||
|
try {
|
|||
|
resolved = new URL(specifier)
|
|||
|
} catch {
|
|||
|
if (!isRemote) {
|
|||
|
resolved = packageResolve(specifier, base, conditions)
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
assert(resolved !== undefined, 'expected to be defined')
|
|||
|
|
|||
|
if (resolved.protocol !== 'file:') {
|
|||
|
return resolved
|
|||
|
}
|
|||
|
|
|||
|
return finalizeResolution(resolved, base, preserveSymlinks)
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* @param {string} specifier
|
|||
|
* @param {URL | undefined} parsed
|
|||
|
* @param {URL | undefined} parsedParentURL
|
|||
|
*/
|
|||
|
function checkIfDisallowedImport(specifier, parsed, parsedParentURL) {
|
|||
|
if (parsedParentURL) {
|
|||
|
// Avoid accessing the `protocol` property due to the lazy getters.
|
|||
|
const parentProtocol = parsedParentURL.protocol
|
|||
|
|
|||
|
if (parentProtocol === 'http:' || parentProtocol === 'https:') {
|
|||
|
if (shouldBeTreatedAsRelativeOrAbsolutePath(specifier)) {
|
|||
|
// Avoid accessing the `protocol` property due to the lazy getters.
|
|||
|
const parsedProtocol = parsed?.protocol
|
|||
|
|
|||
|
// `data:` and `blob:` disallowed due to allowing file: access via
|
|||
|
// indirection
|
|||
|
if (
|
|||
|
parsedProtocol &&
|
|||
|
parsedProtocol !== 'https:' &&
|
|||
|
parsedProtocol !== 'http:'
|
|||
|
) {
|
|||
|
throw new ERR_NETWORK_IMPORT_DISALLOWED(
|
|||
|
specifier,
|
|||
|
parsedParentURL,
|
|||
|
'remote imports cannot import from a local location.'
|
|||
|
)
|
|||
|
}
|
|||
|
|
|||
|
return {url: parsed?.href || ''}
|
|||
|
}
|
|||
|
|
|||
|
if (builtinModules.includes(specifier)) {
|
|||
|
throw new ERR_NETWORK_IMPORT_DISALLOWED(
|
|||
|
specifier,
|
|||
|
parsedParentURL,
|
|||
|
'remote imports cannot import from a local location.'
|
|||
|
)
|
|||
|
}
|
|||
|
|
|||
|
throw new ERR_NETWORK_IMPORT_DISALLOWED(
|
|||
|
specifier,
|
|||
|
parsedParentURL,
|
|||
|
'only relative and absolute specifiers are supported.'
|
|||
|
)
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Note: this is from:
|
|||
|
// <https://github.com/nodejs/node/blob/3e74590/lib/internal/url.js#L687>
|
|||
|
/**
|
|||
|
* Checks if a value has the shape of a WHATWG URL object.
|
|||
|
*
|
|||
|
* Using a symbol or instanceof would not be able to recognize URL objects
|
|||
|
* coming from other implementations (e.g. in Electron), so instead we are
|
|||
|
* checking some well known properties for a lack of a better test.
|
|||
|
*
|
|||
|
* We use `href` and `protocol` as they are the only properties that are
|
|||
|
* easy to retrieve and calculate due to the lazy nature of the getters.
|
|||
|
*
|
|||
|
* @template {unknown} Value
|
|||
|
* @param {Value} self
|
|||
|
* @returns {Value is URL}
|
|||
|
*/
|
|||
|
function isURL(self) {
|
|||
|
return Boolean(
|
|||
|
self &&
|
|||
|
typeof self === 'object' &&
|
|||
|
'href' in self &&
|
|||
|
typeof self.href === 'string' &&
|
|||
|
'protocol' in self &&
|
|||
|
typeof self.protocol === 'string' &&
|
|||
|
self.href &&
|
|||
|
self.protocol
|
|||
|
)
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Validate user-input in `context` supplied by a custom loader.
|
|||
|
*
|
|||
|
* @param {unknown} parentURL
|
|||
|
* @returns {asserts parentURL is URL | string | undefined}
|
|||
|
*/
|
|||
|
function throwIfInvalidParentURL(parentURL) {
|
|||
|
if (parentURL === undefined) {
|
|||
|
return // Main entry point, so no parent
|
|||
|
}
|
|||
|
|
|||
|
if (typeof parentURL !== 'string' && !isURL(parentURL)) {
|
|||
|
throw new codes.ERR_INVALID_ARG_TYPE(
|
|||
|
'parentURL',
|
|||
|
['string', 'URL'],
|
|||
|
parentURL
|
|||
|
)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* @param {string} specifier
|
|||
|
* @param {{parentURL?: string, conditions?: Array<string>}} context
|
|||
|
* @returns {{url: string, format?: string | null}}
|
|||
|
*/
|
|||
|
export function defaultResolve(specifier, context = {}) {
|
|||
|
const {parentURL} = context
|
|||
|
assert(parentURL !== undefined, 'expected `parentURL` to be defined')
|
|||
|
throwIfInvalidParentURL(parentURL)
|
|||
|
|
|||
|
/** @type {URL | undefined} */
|
|||
|
let parsedParentURL
|
|||
|
if (parentURL) {
|
|||
|
try {
|
|||
|
parsedParentURL = new URL(parentURL)
|
|||
|
} catch {
|
|||
|
// Ignore exception
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/** @type {URL | undefined} */
|
|||
|
let parsed
|
|||
|
try {
|
|||
|
parsed = shouldBeTreatedAsRelativeOrAbsolutePath(specifier)
|
|||
|
? new URL(specifier, parsedParentURL)
|
|||
|
: new URL(specifier)
|
|||
|
|
|||
|
// Avoid accessing the `protocol` property due to the lazy getters.
|
|||
|
const protocol = parsed.protocol
|
|||
|
|
|||
|
if (protocol === 'data:') {
|
|||
|
return {url: parsed.href, format: null}
|
|||
|
}
|
|||
|
} catch {
|
|||
|
// Ignore exception
|
|||
|
}
|
|||
|
|
|||
|
// There are multiple deep branches that can either throw or return; instead
|
|||
|
// of duplicating that deeply nested logic for the possible returns, DRY and
|
|||
|
// check for a return. This seems the least gnarly.
|
|||
|
const maybeReturn = checkIfDisallowedImport(
|
|||
|
specifier,
|
|||
|
parsed,
|
|||
|
parsedParentURL
|
|||
|
)
|
|||
|
|
|||
|
if (maybeReturn) return maybeReturn
|
|||
|
|
|||
|
// This must come after checkIfDisallowedImport
|
|||
|
if (parsed && parsed.protocol === 'node:') return {url: specifier}
|
|||
|
|
|||
|
const conditions = getConditionsSet(context.conditions)
|
|||
|
|
|||
|
const url = moduleResolve(specifier, new URL(parentURL), conditions, false)
|
|||
|
|
|||
|
return {
|
|||
|
// Do NOT cast `url` to a string: that will work even when there are real
|
|||
|
// problems, silencing them
|
|||
|
url: url.href,
|
|||
|
format: defaultGetFormatWithoutErrors(url, {parentURL})
|
|||
|
}
|
|||
|
}
|