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+ const path = isWindows ? require ( 'node:path' ) . win32 : require ( 'node:path' )
7+ const { homedir } = require ( 'node:os' )
88const HostedGit = require ( 'hosted-git-info' )
99const semver = require ( 'semver' )
10- const path = global . FAKE_WINDOWS ? require ( 'path' ) . win32 : require ( 'path' )
1110const validatePackageName = require ( 'validate-npm-package-name' )
12- const { homedir } = require ( 'os' )
1311const { log } = require ( 'proc-log' )
1412
15- const isWindows = process . platform === 'win32' || global . FAKE_WINDOWS
1613const hasSlashes = isWindows ? / \\ | [ / ] / : / [ / ] /
1714const isURL = / ^ (?: g i t [ + ] ) ? [ a - z ] + : / i
1815const isGit = / ^ [ ^ @ ] + @ [ ^ : . ] + \. [ ^ : ] + : .+ $ / i
19- const isFilename = / [ . ] (?: t g z | t a r .g z | t a r ) $ / i
16+ const isFileType = / [ . ] (?: t g z | t a r .g z | t a r ) $ / i
2017const isPortNumber = / : [ 0 - 9 ] + ( \/ | $ ) / i
18+ const isWindowsFile = / ^ (?: [ . ] | ~ [ / ] | [ / \\ ] | [ a - z A - Z ] : ) /
19+ const isPosixFile = / ^ (?: [ . ] | ~ [ / ] | [ / ] | [ a - z A - Z ] : ) /
20+ const defaultRegistry = 'https://registry.npmjs.org'
2121
2222function npa ( arg , where ) {
2323 let name
@@ -31,13 +31,14 @@ function npa (arg, where) {
3131 return npa ( arg . raw , where || arg . where )
3232 }
3333 }
34- const nameEndsAt = arg [ 0 ] === '@' ? arg . slice ( 1 ) . indexOf ( '@' ) + 1 : arg . indexOf ( '@' )
34+ const nameEndsAt = arg . indexOf ( '@' , 1 ) // Skip possible leading @
3535 const namePart = nameEndsAt > 0 ? arg . slice ( 0 , nameEndsAt ) : arg
3636 if ( isURL . test ( arg ) ) {
3737 spec = arg
3838 } else if ( isGit . test ( arg ) ) {
3939 spec = `git+ssh://${ arg } `
40- } else if ( namePart [ 0 ] !== '@' && ( hasSlashes . test ( namePart ) || isFilename . test ( namePart ) ) ) {
40+ // eslint-disable-next-line max-len
41+ } else if ( ! namePart . startsWith ( '@' ) && ( hasSlashes . test ( namePart ) || isFileType . test ( namePart ) ) ) {
4142 spec = arg
4243 } else if ( nameEndsAt > 0 ) {
4344 name = namePart
@@ -54,7 +55,25 @@ function npa (arg, where) {
5455 return resolve ( name , spec , where , arg )
5556}
5657
57- const isFilespec = isWindows ? / ^ (?: [ . ] | ~ [ / ] | [ / \\ ] | [ a - z A - Z ] : ) / : / ^ (?: [ . ] | ~ [ / ] | [ / ] | [ a - z A - Z ] : ) /
58+ function isFileSpec ( spec ) {
59+ if ( ! spec ) {
60+ return false
61+ }
62+ if ( spec . toLowerCase ( ) . startsWith ( 'file:' ) ) {
63+ return true
64+ }
65+ if ( isWindows ) {
66+ return isWindowsFile . test ( spec )
67+ }
68+ return isPosixFile . test ( spec )
69+ }
70+
71+ function isAliasSpec ( spec ) {
72+ if ( ! spec ) {
73+ return false
74+ }
75+ return spec . toLowerCase ( ) . startsWith ( 'npm:' )
76+ }
5877
5978function resolve ( name , spec , where , arg ) {
6079 const res = new Result ( {
@@ -65,12 +84,16 @@ function resolve (name, spec, where, arg) {
6584 } )
6685
6786 if ( name ) {
68- res . setName ( name )
87+ res . name = name
6988 }
7089
71- if ( spec && ( isFilespec . test ( spec ) || / ^ f i l e : / i. test ( spec ) ) ) {
90+ if ( ! where ) {
91+ where = process . cwd ( )
92+ }
93+
94+ if ( isFileSpec ( spec ) ) {
7295 return fromFile ( res , where )
73- } else if ( spec && / ^ n p m : / i . test ( spec ) ) {
96+ } else if ( isAliasSpec ( spec ) ) {
7497 return fromAlias ( res , where )
7598 }
7699
@@ -82,15 +105,13 @@ function resolve (name, spec, where, arg) {
82105 return fromHostedGit ( res , hosted )
83106 } else if ( spec && isURL . test ( spec ) ) {
84107 return fromURL ( res )
85- } else if ( spec && ( hasSlashes . test ( spec ) || isFilename . test ( spec ) ) ) {
108+ } else if ( spec && ( hasSlashes . test ( spec ) || isFileType . test ( spec ) ) ) {
86109 return fromFile ( res , where )
87110 } else {
88111 return fromRegistry ( res )
89112 }
90113}
91114
92- const defaultRegistry = 'https://registry.npmjs.org'
93-
94115function toPurl ( arg , reg = defaultRegistry ) {
95116 const res = npa ( arg )
96117
@@ -128,60 +149,59 @@ function invalidPurlType (type, raw) {
128149 return err
129150}
130151
131- function Result ( opts ) {
132- this . type = opts . type
133- this . registry = opts . registry
134- this . where = opts . where
135- if ( opts . raw == null ) {
136- this . raw = opts . name ? opts . name + '@' + opts . rawSpec : opts . rawSpec
137- } else {
138- this . raw = opts . raw
152+ class Result {
153+ constructor ( opts ) {
154+ this . type = opts . type
155+ this . registry = opts . registry
156+ this . where = opts . where
157+ if ( opts . raw == null ) {
158+ this . raw = opts . name ? `${ opts . name } @${ opts . rawSpec } ` : opts . rawSpec
159+ } else {
160+ this . raw = opts . raw
161+ }
162+ this . rawSpec = opts . rawSpec || ''
163+ this . saveSpec = opts . saveSpec
164+ this . fetchSpec = opts . fetchSpec
165+ if ( opts . name ) {
166+ this . setName ( opts . name )
167+ }
168+ this . gitRange = opts . gitRange
169+ this . gitCommittish = opts . gitCommittish
170+ this . gitSubdir = opts . gitSubdir
171+ this . hosted = opts . hosted
139172 }
140173
141- this . name = undefined
142- this . escapedName = undefined
143- this . scope = undefined
144- this . rawSpec = opts . rawSpec || ''
145- this . saveSpec = opts . saveSpec
146- this . fetchSpec = opts . fetchSpec
147- if ( opts . name ) {
148- this . setName ( opts . name )
149- }
150- this . gitRange = opts . gitRange
151- this . gitCommittish = opts . gitCommittish
152- this . gitSubdir = opts . gitSubdir
153- this . hosted = opts . hosted
154- }
174+ // TODO move this to a getter/setter in a semver major
175+ setName ( name ) {
176+ const valid = validatePackageName ( name )
177+ if ( ! valid . validForOldPackages ) {
178+ throw invalidPackageName ( name , valid , this . raw )
179+ }
155180
156- Result . prototype . setName = function ( name ) {
157- const valid = validatePackageName ( name )
158- if ( ! valid . validForOldPackages ) {
159- throw invalidPackageName ( name , valid , this . raw )
181+ this . name = name
182+ this . scope = name [ 0 ] === '@' ? name . slice ( 0 , name . indexOf ( '/' ) ) : undefined
183+ // scoped packages in couch must have slash url-encoded, e.g. @foo%2Fbar
184+ this . escapedName = name . replace ( '/' , '%2f' )
185+ return this
160186 }
161187
162- this . name = name
163- this . scope = name [ 0 ] === '@' ? name . slice ( 0 , name . indexOf ( '/' ) ) : undefined
164- // scoped packages in couch must have slash url-encoded, e.g. @foo%2Fbar
165- this . escapedName = name . replace ( '/' , '%2f' )
166- return this
167- }
168-
169- Result . prototype . toString = function ( ) {
170- const full = [ ]
171- if ( this . name != null && this . name !== '' ) {
172- full . push ( this . name )
173- }
174- const spec = this . saveSpec || this . fetchSpec || this . rawSpec
175- if ( spec != null && spec !== '' ) {
176- full . push ( spec )
188+ toString ( ) {
189+ const full = [ ]
190+ if ( this . name != null && this . name !== '' ) {
191+ full . push ( this . name )
192+ }
193+ const spec = this . saveSpec || this . fetchSpec || this . rawSpec
194+ if ( spec != null && spec !== '' ) {
195+ full . push ( spec )
196+ }
197+ return full . length ? full . join ( '@' ) : this . raw
177198 }
178- return full . length ? full . join ( '@' ) : this . raw
179- }
180199
181- Result . prototype . toJSON = function ( ) {
182- const result = Object . assign ( { } , this )
183- delete result . hosted
184- return result
200+ toJSON ( ) {
201+ const result = Object . assign ( { } , this )
202+ delete result . hosted
203+ return result
204+ }
185205}
186206
187207// sets res.gitCommittish, res.gitRange, and res.gitSubdir
@@ -228,25 +248,89 @@ function setGitAttrs (res, committish) {
228248 }
229249}
230250
231- function fromFile ( res , where ) {
232- if ( ! where ) {
233- where = process . cwd ( )
251+ // Taken from: EncodePathChars and lookup_table in src/node_url.cc
252+ // url.pathToFileURL only returns absolute references. We can't use it to encode paths.
253+ // encodeURI mangles windows paths. We can't use it to encode paths.
254+ // Under the hood, url.pathToFileURL does a limited set of encoding, with an extra windows step, and then calls path.resolve.
255+ // The encoding node does without path.resolve is not available outside of the source, so we are recreating it here.
256+ const encodedPathChars = new Map ( [
257+ [ '\0' , '%00' ] ,
258+ [ '\t' , '%09' ] ,
259+ [ '\n' , '%0A' ] ,
260+ [ '\r' , '%0D' ] ,
261+ [ ' ' , '%20' ] ,
262+ [ '"' , '%22' ] ,
263+ [ '#' , '%23' ] ,
264+ [ '%' , '%25' ] ,
265+ [ '?' , '%3F' ] ,
266+ [ '[' , '%5B' ] ,
267+ [ '\\' , isWindows ? '/' : '%5C' ] ,
268+ [ ']' , '%5D' ] ,
269+ [ '^' , '%5E' ] ,
270+ [ '|' , '%7C' ] ,
271+ [ '~' , '%7E' ] ,
272+ ] )
273+
274+ function pathToFileURL ( str ) {
275+ let result = ''
276+ for ( let i = 0 ; i < str . length ; i ++ ) {
277+ result = `${ result } ${ encodedPathChars . get ( str [ i ] ) ?? str [ i ] } `
234278 }
235- res . type = isFilename . test ( res . rawSpec ) ? 'file' : 'directory'
279+ return `file:${ result } `
280+ }
281+
282+ /* parse file package args:
283+ *
284+ * /posix/path
285+ * ./posix/path
286+ * .dotfile
287+ * .dot/path
288+ * filename
289+ * filename.with.ext
290+ * C:\windows\path
291+ * path/with/no/leading/separator
292+ *
293+ * translates to relative ./path
294+ * - file:{path}
295+ *
296+ * translates to absolute /path
297+ * - file:/{path}
298+ * - file://{path} (this is not RFC compliant, but is supported for backward compatibility)
299+ * - file:///{path}
300+ *
301+ * file: specs are url encoded, bare paths are not
302+ *
303+ */
304+ function fromFile ( res , where ) {
305+ res . type = isFileType . test ( res . rawSpec ) ? 'file' : 'directory'
236306 res . where = where
237307
238- // always put the '/' on where when resolving urls, or else
239- // file:foo from /path/to/bar goes to /path/to/foo, when we want
240- // it to be /path/to/bar/foo
308+ let rawSpec = res . rawSpec
309+ if ( ! rawSpec . startsWith ( 'file:' ) ) {
310+ rawSpec = pathToFileURL ( rawSpec )
311+ }
312+
313+ if ( rawSpec . startsWith ( 'file:/' ) ) {
314+ // XXX backwards compatibility lack of compliance with RFC 8089
315+
316+ // turn file://path into file:/path
317+ if ( / ^ f i l e : \/ \/ [ ^ / ] / . test ( rawSpec ) ) {
318+ rawSpec = `file:/${ rawSpec . slice ( 5 ) } `
319+ }
320+
321+ // turn file:/../path into file:../path
322+ // for 1 or 3 leading slashes (2 is already ruled out from handling file:// explicitly above)
323+ if ( / ^ \/ { 1 , 3 } \. \. ? ( \/ | $ ) / . test ( rawSpec . slice ( 5 ) ) ) {
324+ rawSpec = rawSpec . replace ( / ^ f i l e : \/ { 1 , 3 } / , 'file:' )
325+ }
326+ }
241327
242- let specUrl
243328 let resolvedUrl
244- const prefix = ( ! / ^ f i l e : / . test ( res . rawSpec ) ? 'file:' : '' )
245- const rawWithPrefix = prefix + res . rawSpec
246- let rawNoPrefix = rawWithPrefix . replace ( / ^ f i l e : / , '' )
329+ let specUrl
247330 try {
248- resolvedUrl = new URL ( rawWithPrefix , `file://${ path . resolve ( where ) } /` )
249- specUrl = new URL ( rawWithPrefix )
331+ // 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
332+ resolvedUrl = new URL ( rawSpec , `${ pathToFileURL ( path . resolve ( where ) ) } /` )
333+ specUrl = new URL ( rawSpec )
250334 } catch ( originalError ) {
251335 const er = new Error ( 'Invalid file: URL, must comply with RFC 8089' )
252336 throw Object . assign ( er , {
@@ -257,24 +341,6 @@ function fromFile (res, where) {
257341 } )
258342 }
259343
260- // XXX backwards compatibility lack of compliance with RFC 8089
261- if ( resolvedUrl . host && resolvedUrl . host !== 'localhost' ) {
262- const rawSpec = res . rawSpec . replace ( / ^ f i l e : \/ \/ / , 'file:///' )
263- resolvedUrl = new URL ( rawSpec , `file://${ path . resolve ( where ) } /` )
264- specUrl = new URL ( rawSpec )
265- rawNoPrefix = rawSpec . replace ( / ^ f i l e : / , '' )
266- }
267- // turn file:/../foo into file:../foo
268- // for 1, 2 or 3 leading slashes since we attempted
269- // in the previous step to make it a file protocol url with a leading slash
270- if ( / ^ \/ { 1 , 3 } \. \. ? ( \/ | $ ) / . test ( rawNoPrefix ) ) {
271- const rawSpec = res . rawSpec . replace ( / ^ f i l e : \/ { 1 , 3 } / , 'file:' )
272- resolvedUrl = new URL ( rawSpec , `file://${ path . resolve ( where ) } /` )
273- specUrl = new URL ( rawSpec )
274- rawNoPrefix = rawSpec . replace ( / ^ f i l e : / , '' )
275- }
276- // XXX end RFC 8089 violation backwards compatibility section
277-
278344 // turn /C:/blah into just C:/blah on windows
279345 let specPath = decodeURIComponent ( specUrl . pathname )
280346 let resolvedPath = decodeURIComponent ( resolvedUrl . pathname )
@@ -288,7 +354,7 @@ function fromFile (res, where) {
288354 if ( / ^ \/ ~ ( \/ | $ ) / . test ( specPath ) ) {
289355 res . saveSpec = `file:${ specPath . substr ( 1 ) } `
290356 resolvedPath = path . resolve ( homedir ( ) , specPath . substr ( 3 ) )
291- } else if ( ! path . isAbsolute ( rawNoPrefix ) ) {
357+ } else if ( ! path . isAbsolute ( rawSpec . slice ( 5 ) ) ) {
292358 res . saveSpec = `file:${ path . relative ( where , resolvedPath ) } `
293359 } else {
294360 res . saveSpec = `file:${ path . resolve ( resolvedPath ) } `
@@ -416,3 +482,8 @@ function fromRegistry (res) {
416482 }
417483 return res
418484}
485+
486+ module . exports = npa
487+ module . exports . resolve = resolve
488+ module . exports . toPurl = toPurl
489+ module . exports . Result = Result
0 commit comments