1
1
'use strict'
2
- module . exports = npa
3
- module . exports . resolve = resolve
4
- module . exports . toPurl = toPurl
5
- module . exports . Result = Result
6
2
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' )
8
9
const HostedGit = require ( 'hosted-git-info' )
9
10
const semver = require ( 'semver' )
10
- const path = global . FAKE_WINDOWS ? require ( 'path' ) . win32 : require ( 'path' )
11
11
const validatePackageName = require ( 'validate-npm-package-name' )
12
- const { homedir } = require ( 'os' )
13
12
const { log } = require ( 'proc-log' )
14
13
15
- const isWindows = process . platform === 'win32' || global . FAKE_WINDOWS
16
14
const hasSlashes = isWindows ? / \\ | [ / ] / : / [ / ] /
17
15
const isURL = / ^ (?: g i t [ + ] ) ? [ a - z ] + : / i
18
16
const 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'
20
22
21
23
function npa ( arg , where ) {
22
24
let name
@@ -30,13 +32,14 @@ function npa (arg, where) {
30
32
return npa ( arg . raw , where || arg . where )
31
33
}
32
34
}
33
- const nameEndsAt = arg [ 0 ] === '@' ? arg . slice ( 1 ) . indexOf ( '@' ) + 1 : arg . indexOf ( '@' )
35
+ const nameEndsAt = arg . indexOf ( '@' , 1 ) // Skip possible leading @
34
36
const namePart = nameEndsAt > 0 ? arg . slice ( 0 , nameEndsAt ) : arg
35
37
if ( isURL . test ( arg ) ) {
36
38
spec = arg
37
39
} else if ( isGit . test ( arg ) ) {
38
40
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 ) ) ) {
40
43
spec = arg
41
44
} else if ( nameEndsAt > 0 ) {
42
45
name = namePart
@@ -53,7 +56,27 @@ function npa (arg, where) {
53
56
return resolve ( name , spec , where , arg )
54
57
}
55
58
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
+ }
57
80
58
81
function resolve ( name , spec , where , arg ) {
59
82
const res = new Result ( {
@@ -64,12 +87,16 @@ function resolve (name, spec, where, arg) {
64
87
} )
65
88
66
89
if ( name ) {
67
- res . setName ( name )
90
+ res . name = name
68
91
}
69
92
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 ) ) {
71
98
return fromFile ( res , where )
72
- } else if ( spec && / ^ n p m : / i . test ( spec ) ) {
99
+ } else if ( isAliasSpec ( spec ) ) {
73
100
return fromAlias ( res , where )
74
101
}
75
102
@@ -81,15 +108,13 @@ function resolve (name, spec, where, arg) {
81
108
return fromHostedGit ( res , hosted )
82
109
} else if ( spec && isURL . test ( spec ) ) {
83
110
return fromURL ( res )
84
- } else if ( spec && ( hasSlashes . test ( spec ) || isFilename . test ( spec ) ) ) {
111
+ } else if ( spec && ( hasSlashes . test ( spec ) || isFileType . test ( spec ) ) ) {
85
112
return fromFile ( res , where )
86
113
} else {
87
114
return fromRegistry ( res )
88
115
}
89
116
}
90
117
91
- const defaultRegistry = 'https://registry.npmjs.org'
92
-
93
118
function toPurl ( arg , reg = defaultRegistry ) {
94
119
const res = npa ( arg )
95
120
@@ -127,60 +152,62 @@ function invalidPurlType (type, raw) {
127
152
return err
128
153
}
129
154
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
138
178
}
139
179
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
+ }
154
186
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
159
192
}
160
193
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
176
204
}
177
- return full . length ? full . join ( '@' ) : this . raw
178
- }
179
205
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
+ }
184
211
}
185
212
186
213
// sets res.gitCommittish, res.gitRange, and res.gitSubdir
@@ -227,25 +254,67 @@ function setGitAttrs (res, committish) {
227
254
}
228
255
}
229
256
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
233
287
}
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'
235
293
res . where = where
236
294
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
+ }
240
311
241
- let specUrl
242
312
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
246
314
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 )
249
318
} catch ( originalError ) {
250
319
const er = new Error ( 'Invalid file: URL, must comply with RFC 8089' )
251
320
throw Object . assign ( er , {
@@ -256,24 +325,6 @@ function fromFile (res, where) {
256
325
} )
257
326
}
258
327
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
-
277
328
// turn /C:/blah into just C:/blah on windows
278
329
let specPath = decodeURIComponent ( specUrl . pathname )
279
330
let resolvedPath = decodeURIComponent ( resolvedUrl . pathname )
@@ -287,13 +338,21 @@ function fromFile (res, where) {
287
338
if ( / ^ \/ ~ ( \/ | $ ) / . test ( specPath ) ) {
288
339
res . saveSpec = `file:${ specPath . substr ( 1 ) } `
289
340
resolvedPath = path . resolve ( homedir ( ) , specPath . substr ( 3 ) )
290
- } else if ( ! path . isAbsolute ( rawNoPrefix ) ) {
341
+ } else if ( ! path . isAbsolute ( rawSpec . slice ( 5 ) ) ) {
291
342
res . saveSpec = `file:${ path . relative ( where , resolvedPath ) } `
292
343
} else {
293
344
res . saveSpec = `file:${ path . resolve ( resolvedPath ) } `
294
345
}
295
346
296
347
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
+ }
297
356
return res
298
357
}
299
358
@@ -324,7 +383,9 @@ function fromURL (res) {
324
383
// git+ssh://[email protected] :username/project.git#deadbeef
325
384
// ...and various combinations. The username in the beginning is *required*.
326
385
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 ) ) {
328
389
res . type = 'git'
329
390
setGitAttrs ( res , matched [ 2 ] )
330
391
res . fetchSpec = matched [ 1 ]
@@ -413,3 +474,8 @@ function fromRegistry (res) {
413
474
}
414
475
return res
415
476
}
477
+
478
+ module . exports = npa
479
+ module . exports . resolve = resolve
480
+ module . exports . toPurl = toPurl
481
+ module . exports . Result = Result
0 commit comments