2017-05-09 14:46:02 -07:00
'use strict'
2025-03-13 05:31:42 -07:00
const isWindows = process . platform === 'win32'
const { URL } = require ( 'node:url' )
// We need to use path/win32 so that we get consistent results in tests, but this also means we need to manually convert backslashes to forward slashes when generating file: urls with paths.
const path = isWindows ? require ( 'node:path/win32' ) : require ( 'node:path' )
const { homedir } = require ( 'node:os' )
2021-06-03 20:17:35 +00:00
const HostedGit = require ( 'hosted-git-info' )
const semver = require ( 'semver' )
const validatePackageName = require ( 'validate-npm-package-name' )
2024-04-30 23:53:22 -07:00
const { log } = require ( 'proc-log' )
2017-05-09 14:46:02 -07:00
const hasSlashes = isWindows ? /\\|[/]/ : /[/]/
const isURL = /^(?:git[+])?[a-z]+:/i
2021-02-23 17:29:16 -05:00
const isGit = /^[^@]+@[^:.]+\.[^:]+:.+$/i
2025-03-13 05:31:42 -07:00
const isFileType = /[.](?:tgz|tar.gz|tar)$/i
2025-01-10 08:20:27 -08:00
const isPortNumber = /:[0-9]+(\/|$)/i
2025-03-13 05:31:42 -07:00
const isWindowsFile = /^(?:[.]|~[/]|[/\\]|[a-zA-Z]:)/
const isPosixFile = /^(?:[.]|~[/]|[/]|[a-zA-Z]:)/
const defaultRegistry = 'https://registry.npmjs.org'
2017-05-09 14:46:02 -07:00
function npa ( arg , where ) {
let name
let spec
2017-12-07 14:05:23 -08:00
if ( typeof arg === 'object' ) {
2022-02-24 21:41:49 +00:00
if ( arg instanceof Result && ( ! where || where === arg . where ) ) {
2017-12-07 14:05:23 -08:00
return arg
2022-02-24 21:41:49 +00:00
} else if ( arg . name && arg . rawSpec ) {
2017-12-07 14:05:23 -08:00
return npa . resolve ( arg . name , arg . rawSpec , where || arg . where )
2022-02-24 21:41:49 +00:00
} else {
2017-12-07 14:05:23 -08:00
return npa ( arg . raw , where || arg . where )
2022-02-24 21:41:49 +00:00
}
2017-12-07 14:05:23 -08:00
}
2025-03-13 05:31:42 -07:00
const nameEndsAt = arg . indexOf ( '@' , 1 ) // Skip possible leading @
2017-05-09 14:46:02 -07:00
const namePart = nameEndsAt > 0 ? arg . slice ( 0 , nameEndsAt ) : arg
2022-02-24 21:41:49 +00:00
if ( isURL . test ( arg ) ) {
2017-05-09 14:46:02 -07:00
spec = arg
2022-02-24 21:41:49 +00:00
} else if ( isGit . test ( arg ) ) {
2021-02-23 17:29:16 -05:00
spec = ` git+ssh:// ${ arg } `
2025-03-13 05:31:42 -07:00
// eslint-disable-next-line max-len
} else if ( ! namePart . startsWith ( '@' ) && ( hasSlashes . test ( namePart ) || isFileType . test ( namePart ) ) ) {
2017-05-09 14:46:02 -07:00
spec = arg
2022-02-24 21:41:49 +00:00
} else if ( nameEndsAt > 0 ) {
2017-05-09 14:46:02 -07:00
name = namePart
2022-12-06 22:18:33 -05:00
spec = arg . slice ( nameEndsAt + 1 ) || '*'
2017-05-09 14:46:02 -07:00
} else {
const valid = validatePackageName ( arg )
2022-02-24 21:41:49 +00:00
if ( valid . validForOldPackages ) {
2017-05-09 14:46:02 -07:00
name = arg
2022-12-06 22:18:33 -05:00
spec = '*'
2022-02-24 21:41:49 +00:00
} else {
2017-05-09 14:46:02 -07:00
spec = arg
2022-02-24 21:41:49 +00:00
}
2017-05-09 14:46:02 -07:00
}
return resolve ( name , spec , where , arg )
}
2014-09-24 14:41:07 -07:00
2025-03-13 05:31:42 -07:00
function isFileSpec ( spec ) {
if ( ! spec ) {
return false
}
if ( spec . toLowerCase ( ) . startsWith ( 'file:' ) ) {
return true
}
if ( isWindows ) {
return isWindowsFile . test ( spec )
}
// We never hit this in windows tests, obviously
/* istanbul ignore next */
return isPosixFile . test ( spec )
}
function isAliasSpec ( spec ) {
if ( ! spec ) {
return false
}
return spec . toLowerCase ( ) . startsWith ( 'npm:' )
}
2014-09-24 14:41:07 -07:00
2017-05-09 14:46:02 -07:00
function resolve ( name , spec , where , arg ) {
const res = new Result ( {
raw : arg ,
name : name ,
rawSpec : spec ,
2021-06-03 20:17:35 +00:00
fromArgument : arg != null ,
2017-05-09 14:46:02 -07:00
} )
2014-09-24 14:41:07 -07:00
2022-02-24 21:41:49 +00:00
if ( name ) {
2025-03-13 05:31:42 -07:00
res . name = name
2022-02-24 21:41:49 +00:00
}
2017-05-09 14:46:02 -07:00
2025-03-13 05:31:42 -07:00
if ( ! where ) {
where = process . cwd ( )
}
if ( isFileSpec ( spec ) ) {
2017-05-09 14:46:02 -07:00
return fromFile ( res , where )
2025-03-13 05:31:42 -07:00
} else if ( isAliasSpec ( spec ) ) {
2018-04-20 18:26:37 -07:00
return fromAlias ( res , where )
2022-02-24 21:41:49 +00:00
}
2021-06-03 20:17:35 +00:00
const hosted = HostedGit . fromUrl ( spec , {
noGitPlus : true ,
noCommittish : true ,
} )
2022-02-24 21:41:49 +00:00
if ( hosted ) {
2017-05-09 14:46:02 -07:00
return fromHostedGit ( res , hosted )
2022-02-24 21:41:49 +00:00
} else if ( spec && isURL . test ( spec ) ) {
2017-05-09 14:46:02 -07:00
return fromURL ( res )
2025-03-13 05:31:42 -07:00
} else if ( spec && ( hasSlashes . test ( spec ) || isFileType . test ( spec ) ) ) {
2017-05-09 14:46:02 -07:00
return fromFile ( res , where )
2022-02-24 21:41:49 +00:00
} else {
2017-05-09 14:46:02 -07:00
return fromRegistry ( res )
2022-02-24 21:41:49 +00:00
}
2017-05-09 14:46:02 -07:00
}
2014-09-24 14:41:07 -07:00
2022-12-09 08:38:13 -05:00
function toPurl ( arg , reg = defaultRegistry ) {
const res = npa ( arg )
if ( res . type !== 'version' ) {
throw invalidPurlType ( res . type , res . raw )
}
// URI-encode leading @ of scoped packages
let purl = 'pkg:npm/' + res . name . replace ( /^@/ , '%40' ) + '@' + res . rawSpec
if ( reg !== defaultRegistry ) {
purl += '?repository_url=' + reg
}
return purl
}
2022-02-24 21:41:49 +00:00
function invalidPackageName ( name , valid , raw ) {
// eslint-disable-next-line max-len
const err = new Error ( ` Invalid package name " ${ name } " of package " ${ raw } ": ${ valid . errors . join ( '; ' ) } . ` )
2017-05-09 14:46:02 -07:00
err . code = 'EINVALIDPACKAGENAME'
return err
}
2022-02-24 21:41:49 +00:00
function invalidTagName ( name , raw ) {
// eslint-disable-next-line max-len
const err = new Error ( ` Invalid tag name " ${ name } " of package " ${ raw } ": Tags may not have any characters that encodeURIComponent encodes. ` )
2017-05-09 14:46:02 -07:00
err . code = 'EINVALIDTAGNAME'
return err
}
2014-09-24 14:41:07 -07:00
2022-12-09 08:38:13 -05:00
function invalidPurlType ( type , raw ) {
// eslint-disable-next-line max-len
const err = new Error ( ` Invalid type " ${ type } " of package " ${ raw } ": Purl can only be generated for "version" types. ` )
err . code = 'EINVALIDPURLTYPE'
return err
}
2025-03-13 05:31:42 -07:00
class Result {
constructor ( opts ) {
this . type = opts . type
this . registry = opts . registry
this . where = opts . where
if ( opts . raw == null ) {
this . raw = opts . name ? ` ${ opts . name } @ ${ opts . rawSpec } ` : opts . rawSpec
} else {
this . raw = opts . raw
}
this . name = undefined
this . escapedName = undefined
this . scope = undefined
this . rawSpec = opts . rawSpec || ''
this . saveSpec = opts . saveSpec
this . fetchSpec = opts . fetchSpec
if ( opts . name ) {
this . setName ( opts . name )
}
this . gitRange = opts . gitRange
this . gitCommittish = opts . gitCommittish
this . gitSubdir = opts . gitSubdir
this . hosted = opts . hosted
2022-02-24 21:41:49 +00:00
}
2021-06-03 20:17:35 +00:00
2025-03-13 05:31:42 -07:00
// TODO move this to a getter/setter in a semver major
setName ( name ) {
const valid = validatePackageName ( name )
if ( ! valid . validForOldPackages ) {
throw invalidPackageName ( name , valid , this . raw )
}
2014-09-24 14:41:07 -07:00
2025-03-13 05:31:42 -07:00
this . name = name
this . scope = name [ 0 ] === '@' ? name . slice ( 0 , name . indexOf ( '/' ) ) : undefined
// scoped packages in couch must have slash url-encoded, e.g. @foo%2Fbar
this . escapedName = name . replace ( '/' , '%2f' )
return this
2022-02-24 21:41:49 +00:00
}
2021-06-03 20:17:35 +00:00
2025-03-13 05:31:42 -07:00
toString ( ) {
const full = [ ]
if ( this . name != null && this . name !== '' ) {
full . push ( this . name )
}
const spec = this . saveSpec || this . fetchSpec || this . rawSpec
if ( spec != null && spec !== '' ) {
full . push ( spec )
}
return full . length ? full . join ( '@' ) : this . raw
2022-02-24 21:41:49 +00:00
}
2017-05-09 14:46:02 -07:00
2025-03-13 05:31:42 -07:00
toJSON ( ) {
const result = Object . assign ( { } , this )
delete result . hosted
return result
}
2017-05-09 14:46:02 -07:00
}
2023-10-13 06:55:37 -07:00
// sets res.gitCommittish, res.gitRange, and res.gitSubdir
function setGitAttrs ( res , committish ) {
2022-07-19 08:51:49 -07:00
if ( ! committish ) {
2017-05-09 14:46:02 -07:00
res . gitCommittish = null
2023-10-13 06:55:37 -07:00
return
2022-07-19 08:51:49 -07:00
}
// for each :: separated item:
for ( const part of committish . split ( '::' ) ) {
// if the item has no : the n it is a commit-ish
if ( ! part . includes ( ':' ) ) {
if ( res . gitRange ) {
throw new Error ( 'cannot override existing semver range with a committish' )
}
if ( res . gitCommittish ) {
throw new Error ( 'cannot override existing committish with a second committish' )
}
res . gitCommittish = part
continue
}
// split on name:value
const [ name , value ] = part . split ( ':' )
// if name is semver do semver lookup of ref or tag
if ( name === 'semver' ) {
if ( res . gitCommittish ) {
throw new Error ( 'cannot override existing committish with a semver range' )
}
if ( res . gitRange ) {
throw new Error ( 'cannot override existing semver range with a second semver range' )
}
res . gitRange = decodeURIComponent ( value )
continue
}
if ( name === 'path' ) {
if ( res . gitSubdir ) {
throw new Error ( 'cannot override existing path with a second path' )
}
res . gitSubdir = ` / ${ value } `
continue
}
log . warn ( 'npm-package-arg' , ` ignoring unknown key " ${ name } " ` )
2022-02-24 21:41:49 +00:00
}
2017-05-09 14:46:02 -07:00
}
2014-09-24 14:41:07 -07:00
2025-03-13 05:31:42 -07:00
// Taken from: EncodePathChars and lookup_table in src/node_url.cc
// url.pathToFileURL only returns absolute references. We can't use it to encode paths.
// encodeURI mangles windows paths. We can't use it to encode paths.
// Under the hood, url.pathToFileURL does a limited set of encoding, with an extra windows step, and then calls path.resolve.
// The encoding node does without path.resolve is not available outside of the source, so we are recreating it here.
const encodedPathChars = new Map ( [
[ '\0' , '%00' ] ,
[ '\t' , '%09' ] ,
[ '\n' , '%0A' ] ,
[ '\r' , '%0D' ] ,
[ ' ' , '%20' ] ,
[ '"' , '%22' ] ,
[ '#' , '%23' ] ,
[ '%' , '%25' ] ,
[ '?' , '%3F' ] ,
[ '[' , '%5B' ] ,
[ '\\' , isWindows ? '/' : '%5C' ] ,
[ ']' , '%5D' ] ,
[ '^' , '%5E' ] ,
[ '|' , '%7C' ] ,
[ '~' , '%7E' ] ,
] )
function pathToFileURL ( str ) {
let result = ''
for ( let i = 0 ; i < str . length ; i ++ ) {
result = ` ${ result } ${ encodedPathChars . get ( str [ i ] ) ? ? str [ i ] } `
}
if ( result . startsWith ( 'file:' ) ) {
return result
2022-02-24 21:41:49 +00:00
}
2025-03-13 05:31:42 -07:00
return ` file: ${ result } `
}
function fromFile ( res , where ) {
res . type = isFileType . test ( res . rawSpec ) ? 'file' : 'directory'
2017-05-09 14:46:02 -07:00
res . where = where
2025-03-13 05:31:42 -07:00
let rawSpec = pathToFileURL ( res . rawSpec )
if ( rawSpec . startsWith ( 'file:/' ) ) {
// XXX backwards compatibility lack of compliance with RFC 8089
// turn file://path into file:/path
if ( /^file:\/\/[^/]/ . test ( rawSpec ) ) {
rawSpec = ` file:/ ${ rawSpec . slice ( 5 ) } `
}
// turn file:/../path into file:../path
// for 1 or 3 leading slashes (2 is already ruled out from handling file:// explicitly above)
if ( /^\/{1,3}\.\.?(\/|$)/ . test ( rawSpec . slice ( 5 ) ) ) {
rawSpec = rawSpec . replace ( /^file:\/{1,3}/ , 'file:' )
}
}
2021-06-17 18:59:38 +00:00
let resolvedUrl
2025-03-13 05:31:42 -07:00
let specUrl
2021-06-17 18:59:38 +00:00
try {
2025-03-13 05:31:42 -07:00
// always put the '/' on "where", or else file:foo from /path/to/bar goes to /path/to/foo, when we want it to be /path/to/bar/foo
resolvedUrl = new URL ( rawSpec , ` ${ pathToFileURL ( path . resolve ( where ) ) } / ` )
specUrl = new URL ( rawSpec )
2021-06-17 18:59:38 +00:00
} catch ( originalError ) {
2023-10-13 06:55:37 -07:00
const er = new Error ( 'Invalid file: URL, must comply with RFC 8089' )
2021-06-17 18:59:38 +00:00
throw Object . assign ( er , {
raw : res . rawSpec ,
spec : res ,
where ,
originalError ,
} )
}
// turn /C:/blah into just C:/blah on windows
let specPath = decodeURIComponent ( specUrl . pathname )
let resolvedPath = decodeURIComponent ( resolvedUrl . pathname )
if ( isWindows ) {
specPath = specPath . replace ( /^\/+([a-z]:\/)/i , '$1' )
resolvedPath = resolvedPath . replace ( /^\/+([a-z]:\/)/i , '$1' )
2014-09-24 14:41:07 -07:00
}
2021-06-17 18:59:38 +00:00
// replace ~ with homedir, but keep the ~ in the saveSpec
// otherwise, make it relative to where param
if ( /^\/~(\/|$)/ . test ( specPath ) ) {
res . saveSpec = ` file: ${ specPath . substr ( 1 ) } `
resolvedPath = path . resolve ( homedir ( ) , specPath . substr ( 3 ) )
2025-03-13 05:31:42 -07:00
} else if ( ! path . isAbsolute ( rawSpec . slice ( 5 ) ) ) {
2021-06-17 18:59:38 +00:00
res . saveSpec = ` file: ${ path . relative ( where , resolvedPath ) } `
2022-02-24 21:41:49 +00:00
} else {
2021-06-17 18:59:38 +00:00
res . saveSpec = ` file: ${ path . resolve ( resolvedPath ) } `
2022-02-24 21:41:49 +00:00
}
2021-06-17 18:59:38 +00:00
res . fetchSpec = path . resolve ( where , resolvedPath )
2025-03-13 05:31:42 -07:00
// re-normalize the slashes in saveSpec due to node:path/win32 behavior in windows
res . saveSpec = res . saveSpec . split ( '\\' ) . join ( '/' )
// Ignoring because this only happens in windows
/* istanbul ignore next */
if ( res . saveSpec . startsWith ( 'file://' ) ) {
// normalization of \\win32\root paths can cause a double / which we don't want
res . saveSpec = ` file:/ ${ res . saveSpec . slice ( 7 ) } `
}
2014-09-24 14:41:07 -07:00
return res
}
2017-05-09 14:46:02 -07:00
function fromHostedGit ( res , hosted ) {
res . type = 'git'
res . hosted = hosted
2020-10-02 17:52:19 -04:00
res . saveSpec = hosted . toString ( { noGitPlus : false , noCommittish : false } )
2017-05-09 14:46:02 -07:00
res . fetchSpec = hosted . getDefaultRepresentation ( ) === 'shortcut' ? null : hosted . toString ( )
2023-10-13 06:55:37 -07:00
setGitAttrs ( res , hosted . committish )
return res
2016-06-24 13:43:51 -07:00
}
2017-05-09 14:46:02 -07:00
function unsupportedURLType ( protocol , spec ) {
const err = new Error ( ` Unsupported URL Type " ${ protocol } ": ${ spec } ` )
err . code = 'EUNSUPPORTEDPROTOCOL'
return err
}
function fromURL ( res ) {
2023-10-13 06:55:37 -07:00
let rawSpec = res . rawSpec
res . saveSpec = rawSpec
if ( rawSpec . startsWith ( 'git+ssh:' ) ) {
// git ssh specifiers are overloaded to also use scp-style git
// specifiers, so we have to parse those out and treat them special.
// They are NOT true URIs, so we can't hand them to URL.
// This regex looks for things that look like:
// git+ssh://git@my.custom.git.com:username/project.git#deadbeef
// ...and various combinations. The username in the beginning is *required*.
const matched = rawSpec . match ( /^git\+ssh:\/\/([^:#]+:[^#]+(?:\.git)?)(?:#(.*))?$/i )
2025-01-10 08:20:27 -08:00
// Filter out all-number "usernames" which are really port numbers
// They can either be :1234 :1234/ or :1234/path but not :12abc
if ( matched && ! matched [ 1 ] . match ( isPortNumber ) ) {
2023-10-13 06:55:37 -07:00
res . type = 'git'
setGitAttrs ( res , matched [ 2 ] )
res . fetchSpec = matched [ 1 ]
return res
}
} else if ( rawSpec . startsWith ( 'git+file://' ) ) {
// URL can't handle windows paths
rawSpec = rawSpec . replace ( /\\/g , '/' )
}
const parsedUrl = new URL ( rawSpec )
2014-09-24 14:41:07 -07:00
// check the protocol, and then see if it's git or not
2023-10-13 06:55:37 -07:00
switch ( parsedUrl . protocol ) {
2016-06-24 13:43:51 -07:00
case 'git:' :
case 'git+http:' :
case 'git+https:' :
case 'git+rsync:' :
case 'git+ftp:' :
case 'git+file:' :
2023-10-13 06:55:37 -07:00
case 'git+ssh:' :
2016-06-24 13:43:51 -07:00
res . type = 'git'
2023-10-13 06:55:37 -07:00
setGitAttrs ( res , parsedUrl . hash . slice ( 1 ) )
if ( parsedUrl . protocol === 'git+file:' && /^git\+file:\/\/[a-z]:/i . test ( rawSpec ) ) {
// URL can't handle drive letters on windows file paths, the host can't contain a :
res . fetchSpec = ` git+file:// ${ parsedUrl . host . toLowerCase ( ) } : ${ parsedUrl . pathname } `
2017-06-05 16:31:14 -07:00
} else {
2023-10-13 06:55:37 -07:00
parsedUrl . hash = ''
res . fetchSpec = parsedUrl . toString ( )
}
if ( res . fetchSpec . startsWith ( 'git+' ) ) {
res . fetchSpec = res . fetchSpec . slice ( 4 )
2017-06-05 16:31:14 -07:00
}
2014-09-24 14:41:07 -07:00
break
2016-06-24 13:43:51 -07:00
case 'http:' :
case 'https:' :
res . type = 'remote'
2017-05-09 14:46:02 -07:00
res . fetchSpec = res . saveSpec
2015-03-27 03:56:05 -07:00
break
2014-09-24 14:41:07 -07:00
default :
2023-10-13 06:55:37 -07:00
throw unsupportedURLType ( parsedUrl . protocol , rawSpec )
2014-09-24 14:41:07 -07:00
}
return res
}
2018-04-20 18:26:37 -07:00
function fromAlias ( res , where ) {
const subSpec = npa ( res . rawSpec . substr ( 4 ) , where )
2022-02-24 21:41:49 +00:00
if ( subSpec . type === 'alias' ) {
2018-04-20 18:26:37 -07:00
throw new Error ( 'nested aliases not supported' )
2022-02-24 21:41:49 +00:00
}
2021-06-03 20:17:35 +00:00
2022-02-24 21:41:49 +00:00
if ( ! subSpec . registry ) {
2018-04-20 18:26:37 -07:00
throw new Error ( 'aliases only work for registry deps' )
2022-02-24 21:41:49 +00:00
}
2021-06-03 20:17:35 +00:00
2024-09-07 23:09:40 -07:00
if ( ! subSpec . name ) {
throw new Error ( 'aliases must have a name' )
}
2018-04-20 18:26:37 -07:00
res . subSpec = subSpec
res . registry = true
res . type = 'alias'
res . saveSpec = null
res . fetchSpec = null
return res
}
2017-05-09 14:46:02 -07:00
function fromRegistry ( res ) {
res . registry = true
2022-12-06 22:18:33 -05:00
const spec = res . rawSpec . trim ( )
2017-05-09 14:46:02 -07:00
// no save spec for registry components as we save based on the fetched
// version, not on the argument so this can't compute that.
res . saveSpec = null
res . fetchSpec = spec
const version = semver . valid ( spec , true )
const range = semver . validRange ( spec , true )
2022-02-24 21:41:49 +00:00
if ( version ) {
2017-05-09 14:46:02 -07:00
res . type = 'version'
2022-02-24 21:41:49 +00:00
} else if ( range ) {
2017-05-09 14:46:02 -07:00
res . type = 'range'
2022-02-24 21:41:49 +00:00
} else {
if ( encodeURIComponent ( spec ) !== spec ) {
throw invalidTagName ( spec , res . raw )
}
2017-05-09 14:46:02 -07:00
res . type = 'tag'
}
return res
2014-09-24 14:41:07 -07:00
}
2025-03-13 05:31:42 -07:00
module . exports = npa
module . exports . resolve = resolve
module . exports . toPurl = toPurl
module . exports . Result = Result