11'use strict'
2- module . exports = npa
3- module . exports . resolve = resolve
4- module . exports . toPurl = toPurl
5- module . exports . Result = Result
62
7- const { URL } = require ( 'url' )
3+ const isWindows = process . platform === 'win32'
4+
5+ const { URL } = require ( 'node:url' )
6+ // 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.
7+ const path = isWindows ? require ( 'node:path/win32' ) : require ( 'node:path' )
8+ const { homedir } = require ( 'node:os' )
89const HostedGit = require ( 'hosted-git-info' )
910const semver = require ( 'semver' )
10- const path = global . FAKE_WINDOWS ? require ( 'path' ) . win32 : require ( 'path' )
1111const validatePackageName = require ( 'validate-npm-package-name' )
12- const { homedir } = require ( 'os' )
1312const { log } = require ( 'proc-log' )
1413
15- const isWindows = process . platform === 'win32' || global . FAKE_WINDOWS
1614const hasSlashes = isWindows ? / \\ | [ / ] / : / [ / ] /
1715const isURL = / ^ (?: g i t [ + ] ) ? [ a - z ] + : / i
1816const isGit = / ^ [ ^ @ ] + @ [ ^ : . ] + \. [ ^ : ] + : .+ $ / i
19- const isFilename = / [ . ] (?: t g z | t a r .g z | t a r ) $ / i
17+ const isFileType = / [ . ] (?: t g z | t a r .g z | t a r ) $ / i
18+ const isPortNumber = / : [ 0 - 9 ] + ( \/ | $ ) / i
19+ const isWindowsFile = / ^ (?: [ . ] | ~ [ / ] | [ / \\ ] | [ a - z A - Z ] : ) /
20+ const isPosixFile = / ^ (?: [ . ] | ~ [ / ] | [ / ] | [ a - z A - Z ] : ) /
21+ const defaultRegistry = 'https://registry.npmjs.org'
2022
2123function npa ( arg , where ) {
2224 let name
@@ -30,13 +32,14 @@ function npa (arg, where) {
3032 return npa ( arg . raw , where || arg . where )
3133 }
3234 }
33- const nameEndsAt = arg [ 0 ] === '@' ? arg . slice ( 1 ) . indexOf ( '@' ) + 1 : arg . indexOf ( '@' )
35+ const nameEndsAt = arg . indexOf ( '@' , 1 ) // Skip possible leading @
3436 const namePart = nameEndsAt > 0 ? arg . slice ( 0 , nameEndsAt ) : arg
3537 if ( isURL . test ( arg ) ) {
3638 spec = arg
3739 } else if ( isGit . test ( arg ) ) {
3840 spec = `git+ssh://${ arg } `
39- } else if ( namePart [ 0 ] !== '@' && ( hasSlashes . test ( namePart ) || isFilename . test ( namePart ) ) ) {
41+ // eslint-disable-next-line max-len
42+ } else if ( ! namePart . startsWith ( '@' ) && ( hasSlashes . test ( namePart ) || isFileType . test ( namePart ) ) ) {
4043 spec = arg
4144 } else if ( nameEndsAt > 0 ) {
4245 name = namePart
@@ -53,7 +56,27 @@ function npa (arg, where) {
5356 return resolve ( name , spec , where , arg )
5457}
5558
56- const isFilespec = isWindows ? / ^ (?: [ . ] | ~ [ / ] | [ / \\ ] | [ a - z A - Z ] : ) / : / ^ (?: [ . ] | ~ [ / ] | [ / ] | [ a - z A - Z ] : ) /
59+ function isFileSpec ( spec ) {
60+ if ( ! spec ) {
61+ return false
62+ }
63+ if ( spec . toLowerCase ( ) . startsWith ( 'file:' ) ) {
64+ return true
65+ }
66+ if ( isWindows ) {
67+ return isWindowsFile . test ( spec )
68+ }
69+ // We never hit this in windows tests, obviously
70+ /* istanbul ignore next */
71+ return isPosixFile . test ( spec )
72+ }
73+
74+ function isAliasSpec ( spec ) {
75+ if ( ! spec ) {
76+ return false
77+ }
78+ return spec . toLowerCase ( ) . startsWith ( 'npm:' )
79+ }
5780
5881function resolve ( name , spec , where , arg ) {
5982 const res = new Result ( {
@@ -64,12 +87,16 @@ function resolve (name, spec, where, arg) {
6487 } )
6588
6689 if ( name ) {
67- res . setName ( name )
90+ res . name = name
6891 }
6992
70- if ( spec && ( isFilespec . test ( spec ) || / ^ f i l e : / i. test ( spec ) ) ) {
93+ if ( ! where ) {
94+ where = process . cwd ( )
95+ }
96+
97+ if ( isFileSpec ( spec ) ) {
7198 return fromFile ( res , where )
72- } else if ( spec && / ^ n p m : / i . test ( spec ) ) {
99+ } else if ( isAliasSpec ( spec ) ) {
73100 return fromAlias ( res , where )
74101 }
75102
@@ -81,15 +108,13 @@ function resolve (name, spec, where, arg) {
81108 return fromHostedGit ( res , hosted )
82109 } else if ( spec && isURL . test ( spec ) ) {
83110 return fromURL ( res )
84- } else if ( spec && ( hasSlashes . test ( spec ) || isFilename . test ( spec ) ) ) {
111+ } else if ( spec && ( hasSlashes . test ( spec ) || isFileType . test ( spec ) ) ) {
85112 return fromFile ( res , where )
86113 } else {
87114 return fromRegistry ( res )
88115 }
89116}
90117
91- const defaultRegistry = 'https://registry.npmjs.org'
92-
93118function toPurl ( arg , reg = defaultRegistry ) {
94119 const res = npa ( arg )
95120
@@ -127,60 +152,62 @@ function invalidPurlType (type, raw) {
127152 return err
128153}
129154
130- function Result ( opts ) {
131- this . type = opts . type
132- this . registry = opts . registry
133- this . where = opts . where
134- if ( opts . raw == null ) {
135- this . raw = opts . name ? opts . name + '@' + opts . rawSpec : opts . rawSpec
136- } else {
137- this . raw = opts . raw
155+ class Result {
156+ constructor ( opts ) {
157+ this . type = opts . type
158+ this . registry = opts . registry
159+ this . where = opts . where
160+ if ( opts . raw == null ) {
161+ this . raw = opts . name ? `${ opts . name } @${ opts . rawSpec } ` : opts . rawSpec
162+ } else {
163+ this . raw = opts . raw
164+ }
165+ this . name = undefined
166+ this . escapedName = undefined
167+ this . scope = undefined
168+ this . rawSpec = opts . rawSpec || ''
169+ this . saveSpec = opts . saveSpec
170+ this . fetchSpec = opts . fetchSpec
171+ if ( opts . name ) {
172+ this . setName ( opts . name )
173+ }
174+ this . gitRange = opts . gitRange
175+ this . gitCommittish = opts . gitCommittish
176+ this . gitSubdir = opts . gitSubdir
177+ this . hosted = opts . hosted
138178 }
139179
140- this . name = undefined
141- this . escapedName = undefined
142- this . scope = undefined
143- this . rawSpec = opts . rawSpec || ''
144- this . saveSpec = opts . saveSpec
145- this . fetchSpec = opts . fetchSpec
146- if ( opts . name ) {
147- this . setName ( opts . name )
148- }
149- this . gitRange = opts . gitRange
150- this . gitCommittish = opts . gitCommittish
151- this . gitSubdir = opts . gitSubdir
152- this . hosted = opts . hosted
153- }
180+ // TODO move this to a getter/setter in a semver major
181+ setName ( name ) {
182+ const valid = validatePackageName ( name )
183+ if ( ! valid . validForOldPackages ) {
184+ throw invalidPackageName ( name , valid , this . raw )
185+ }
154186
155- Result . prototype . setName = function ( name ) {
156- const valid = validatePackageName ( name )
157- if ( ! valid . validForOldPackages ) {
158- throw invalidPackageName ( name , valid , this . raw )
187+ this . name = name
188+ this . scope = name [ 0 ] === '@' ? name . slice ( 0 , name . indexOf ( '/' ) ) : undefined
189+ // scoped packages in couch must have slash url-encoded, e.g. @foo%2Fbar
190+ this . escapedName = name . replace ( '/' , '%2f' )
191+ return this
159192 }
160193
161- this . name = name
162- this . scope = name [ 0 ] === '@' ? name . slice ( 0 , name . indexOf ( '/' ) ) : undefined
163- // scoped packages in couch must have slash url-encoded, e.g. @foo%2Fbar
164- this . escapedName = name . replace ( '/' , '%2f' )
165- return this
166- }
167-
168- Result . prototype . toString = function ( ) {
169- const full = [ ]
170- if ( this . name != null && this . name !== '' ) {
171- full . push ( this . name )
172- }
173- const spec = this . saveSpec || this . fetchSpec || this . rawSpec
174- if ( spec != null && spec !== '' ) {
175- full . push ( spec )
194+ toString ( ) {
195+ const full = [ ]
196+ if ( this . name != null && this . name !== '' ) {
197+ full . push ( this . name )
198+ }
199+ const spec = this . saveSpec || this . fetchSpec || this . rawSpec
200+ if ( spec != null && spec !== '' ) {
201+ full . push ( spec )
202+ }
203+ return full . length ? full . join ( '@' ) : this . raw
176204 }
177- return full . length ? full . join ( '@' ) : this . raw
178- }
179205
180- Result . prototype . toJSON = function ( ) {
181- const result = Object . assign ( { } , this )
182- delete result . hosted
183- return result
206+ toJSON ( ) {
207+ const result = Object . assign ( { } , this )
208+ delete result . hosted
209+ return result
210+ }
184211}
185212
186213// sets res.gitCommittish, res.gitRange, and res.gitSubdir
@@ -227,25 +254,67 @@ function setGitAttrs (res, committish) {
227254 }
228255}
229256
230- function fromFile ( res , where ) {
231- if ( ! where ) {
232- where = process . cwd ( )
257+ // Taken from: EncodePathChars and lookup_table in src/node_url.cc
258+ // url.pathToFileURL only returns absolute references. We can't use it to encode paths.
259+ // encodeURI mangles windows paths. We can't use it to encode paths.
260+ // Under the hood, url.pathToFileURL does a limited set of encoding, with an extra windows step, and then calls path.resolve.
261+ // The encoding node does without path.resolve is not available outside of the source, so we are recreating it here.
262+ const encodedPathChars = new Map ( [
263+ [ '\0' , '%00' ] ,
264+ [ '\t' , '%09' ] ,
265+ [ '\n' , '%0A' ] ,
266+ [ '\r' , '%0D' ] ,
267+ [ ' ' , '%20' ] ,
268+ [ '"' , '%22' ] ,
269+ [ '#' , '%23' ] ,
270+ [ '%' , '%25' ] ,
271+ [ '?' , '%3F' ] ,
272+ [ '[' , '%5B' ] ,
273+ [ '\\' , isWindows ? '/' : '%5C' ] ,
274+ [ ']' , '%5D' ] ,
275+ [ '^' , '%5E' ] ,
276+ [ '|' , '%7C' ] ,
277+ [ '~' , '%7E' ] ,
278+ ] )
279+
280+ function pathToFileURL ( str ) {
281+ let result = ''
282+ for ( let i = 0 ; i < str . length ; i ++ ) {
283+ result = `${ result } ${ encodedPathChars . get ( str [ i ] ) ?? str [ i ] } `
284+ }
285+ if ( result . startsWith ( 'file:' ) ) {
286+ return result
233287 }
234- res . type = isFilename . test ( res . rawSpec ) ? 'file' : 'directory'
288+ return `file:${ result } `
289+ }
290+
291+ function fromFile ( res , where ) {
292+ res . type = isFileType . test ( res . rawSpec ) ? 'file' : 'directory'
235293 res . where = where
236294
237- // always put the '/' on where when resolving urls, or else
238- // file:foo from /path/to/bar goes to /path/to/foo, when we want
239- // it to be /path/to/bar/foo
295+ let rawSpec = pathToFileURL ( res . rawSpec )
296+
297+ if ( rawSpec . startsWith ( 'file:/' ) ) {
298+ // XXX backwards compatibility lack of compliance with RFC 8089
299+
300+ // turn file://path into file:/path
301+ if ( / ^ f i l e : \/ \/ [ ^ / ] / . test ( rawSpec ) ) {
302+ rawSpec = `file:/${ rawSpec . slice ( 5 ) } `
303+ }
304+
305+ // turn file:/../path into file:../path
306+ // for 1 or 3 leading slashes (2 is already ruled out from handling file:// explicitly above)
307+ if ( / ^ \/ { 1 , 3 } \. \. ? ( \/ | $ ) / . test ( rawSpec . slice ( 5 ) ) ) {
308+ rawSpec = rawSpec . replace ( / ^ f i l e : \/ { 1 , 3 } / , 'file:' )
309+ }
310+ }
240311
241- let specUrl
242312 let resolvedUrl
243- const prefix = ( ! / ^ f i l e : / . test ( res . rawSpec ) ? 'file:' : '' )
244- const rawWithPrefix = prefix + res . rawSpec
245- let rawNoPrefix = rawWithPrefix . replace ( / ^ f i l e : / , '' )
313+ let specUrl
246314 try {
247- resolvedUrl = new URL ( rawWithPrefix , `file://${ path . resolve ( where ) } /` )
248- specUrl = new URL ( rawWithPrefix )
315+ // 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
316+ resolvedUrl = new URL ( rawSpec , `${ pathToFileURL ( path . resolve ( where ) ) } /` )
317+ specUrl = new URL ( rawSpec )
249318 } catch ( originalError ) {
250319 const er = new Error ( 'Invalid file: URL, must comply with RFC 8089' )
251320 throw Object . assign ( er , {
@@ -256,24 +325,6 @@ function fromFile (res, where) {
256325 } )
257326 }
258327
259- // XXX backwards compatibility lack of compliance with RFC 8089
260- if ( resolvedUrl . host && resolvedUrl . host !== 'localhost' ) {
261- const rawSpec = res . rawSpec . replace ( / ^ f i l e : \/ \/ / , 'file:///' )
262- resolvedUrl = new URL ( rawSpec , `file://${ path . resolve ( where ) } /` )
263- specUrl = new URL ( rawSpec )
264- rawNoPrefix = rawSpec . replace ( / ^ f i l e : / , '' )
265- }
266- // turn file:/../foo into file:../foo
267- // for 1, 2 or 3 leading slashes since we attempted
268- // in the previous step to make it a file protocol url with a leading slash
269- if ( / ^ \/ { 1 , 3 } \. \. ? ( \/ | $ ) / . test ( rawNoPrefix ) ) {
270- const rawSpec = res . rawSpec . replace ( / ^ f i l e : \/ { 1 , 3 } / , 'file:' )
271- resolvedUrl = new URL ( rawSpec , `file://${ path . resolve ( where ) } /` )
272- specUrl = new URL ( rawSpec )
273- rawNoPrefix = rawSpec . replace ( / ^ f i l e : / , '' )
274- }
275- // XXX end RFC 8089 violation backwards compatibility section
276-
277328 // turn /C:/blah into just C:/blah on windows
278329 let specPath = decodeURIComponent ( specUrl . pathname )
279330 let resolvedPath = decodeURIComponent ( resolvedUrl . pathname )
@@ -287,13 +338,21 @@ function fromFile (res, where) {
287338 if ( / ^ \/ ~ ( \/ | $ ) / . test ( specPath ) ) {
288339 res . saveSpec = `file:${ specPath . substr ( 1 ) } `
289340 resolvedPath = path . resolve ( homedir ( ) , specPath . substr ( 3 ) )
290- } else if ( ! path . isAbsolute ( rawNoPrefix ) ) {
341+ } else if ( ! path . isAbsolute ( rawSpec . slice ( 5 ) ) ) {
291342 res . saveSpec = `file:${ path . relative ( where , resolvedPath ) } `
292343 } else {
293344 res . saveSpec = `file:${ path . resolve ( resolvedPath ) } `
294345 }
295346
296347 res . fetchSpec = path . resolve ( where , resolvedPath )
348+ // re-normalize the slashes in saveSpec due to node:path/win32 behavior in windows
349+ res . saveSpec = res . saveSpec . split ( '\\' ) . join ( '/' )
350+ // Ignoring because this only happens in windows
351+ /* istanbul ignore next */
352+ if ( res . saveSpec . startsWith ( 'file://' ) ) {
353+ // normalization of \\win32\root paths can cause a double / which we don't want
354+ res . saveSpec = `file:/${ res . saveSpec . slice ( 7 ) } `
355+ }
297356 return res
298357}
299358
@@ -324,7 +383,9 @@ function fromURL (res) {
324383 // git+ssh://git@my.custom.git.com:username/project.git#deadbeef
325384 // ...and various combinations. The username in the beginning is *required*.
326385 const matched = rawSpec . match ( / ^ g i t \+ s s h : \/ \/ ( [ ^ : # ] + : [ ^ # ] + (?: \. g i t ) ? ) (?: # ( .* ) ) ? $ / i)
327- if ( matched && ! matched [ 1 ] . match ( / : [ 0 - 9 ] + \/ ? .* $ / i) ) {
386+ // Filter out all-number "usernames" which are really port numbers
387+ // They can either be :1234 :1234/ or :1234/path but not :12abc
388+ if ( matched && ! matched [ 1 ] . match ( isPortNumber ) ) {
328389 res . type = 'git'
329390 setGitAttrs ( res , matched [ 2 ] )
330391 res . fetchSpec = matched [ 1 ]
@@ -413,3 +474,8 @@ function fromRegistry (res) {
413474 }
414475 return res
415476}
477+
478+ module . exports = npa
479+ module . exports . resolve = resolve
480+ module . exports . toPurl = toPurl
481+ module . exports . Result = Result
0 commit comments