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
20
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'
21
22
22
23
function npa ( arg , where ) {
23
24
let name
@@ -31,13 +32,14 @@ function npa (arg, where) {
31
32
return npa ( arg . raw , where || arg . where )
32
33
}
33
34
}
34
- const nameEndsAt = arg [ 0 ] === '@' ? arg . slice ( 1 ) . indexOf ( '@' ) + 1 : arg . indexOf ( '@' )
35
+ const nameEndsAt = arg . indexOf ( '@' , 1 ) // Skip possible leading @
35
36
const namePart = nameEndsAt > 0 ? arg . slice ( 0 , nameEndsAt ) : arg
36
37
if ( isURL . test ( arg ) ) {
37
38
spec = arg
38
39
} else if ( isGit . test ( arg ) ) {
39
40
spec = `git+ssh://${ arg } `
40
- } 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 ) ) ) {
41
43
spec = arg
42
44
} else if ( nameEndsAt > 0 ) {
43
45
name = namePart
@@ -54,7 +56,27 @@ function npa (arg, where) {
54
56
return resolve ( name , spec , where , arg )
55
57
}
56
58
57
- 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
+ }
58
80
59
81
function resolve ( name , spec , where , arg ) {
60
82
const res = new Result ( {
@@ -65,12 +87,16 @@ function resolve (name, spec, where, arg) {
65
87
} )
66
88
67
89
if ( name ) {
68
- res . setName ( name )
90
+ res . name = name
69
91
}
70
92
71
- 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 ) ) {
72
98
return fromFile ( res , where )
73
- } else if ( spec && / ^ n p m : / i . test ( spec ) ) {
99
+ } else if ( isAliasSpec ( spec ) ) {
74
100
return fromAlias ( res , where )
75
101
}
76
102
@@ -82,15 +108,13 @@ function resolve (name, spec, where, arg) {
82
108
return fromHostedGit ( res , hosted )
83
109
} else if ( spec && isURL . test ( spec ) ) {
84
110
return fromURL ( res )
85
- } else if ( spec && ( hasSlashes . test ( spec ) || isFilename . test ( spec ) ) ) {
111
+ } else if ( spec && ( hasSlashes . test ( spec ) || isFileType . test ( spec ) ) ) {
86
112
return fromFile ( res , where )
87
113
} else {
88
114
return fromRegistry ( res )
89
115
}
90
116
}
91
117
92
- const defaultRegistry = 'https://registry.npmjs.org'
93
-
94
118
function toPurl ( arg , reg = defaultRegistry ) {
95
119
const res = npa ( arg )
96
120
@@ -128,60 +152,62 @@ function invalidPurlType (type, raw) {
128
152
return err
129
153
}
130
154
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
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
139
178
}
140
179
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
- }
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
+ }
155
186
156
- Result . prototype . setName = function ( name ) {
157
- const valid = validatePackageName ( name )
158
- if ( ! valid . validForOldPackages ) {
159
- 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
160
192
}
161
193
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 )
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
177
204
}
178
- return full . length ? full . join ( '@' ) : this . raw
179
- }
180
205
181
- Result . prototype . toJSON = function ( ) {
182
- const result = Object . assign ( { } , this )
183
- delete result . hosted
184
- return result
206
+ toJSON ( ) {
207
+ const result = Object . assign ( { } , this )
208
+ delete result . hosted
209
+ return result
210
+ }
185
211
}
186
212
187
213
// sets res.gitCommittish, res.gitRange, and res.gitSubdir
@@ -228,25 +254,67 @@ function setGitAttrs (res, committish) {
228
254
}
229
255
}
230
256
231
- function fromFile ( res , where ) {
232
- if ( ! where ) {
233
- 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
234
287
}
235
- 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'
236
293
res . where = where
237
294
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
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
+ }
241
311
242
- let specUrl
243
312
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 : / , '' )
313
+ let specUrl
247
314
try {
248
- resolvedUrl = new URL ( rawWithPrefix , `file://${ path . resolve ( where ) } /` )
249
- 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 )
250
318
} catch ( originalError ) {
251
319
const er = new Error ( 'Invalid file: URL, must comply with RFC 8089' )
252
320
throw Object . assign ( er , {
@@ -257,24 +325,6 @@ function fromFile (res, where) {
257
325
} )
258
326
}
259
327
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
-
278
328
// turn /C:/blah into just C:/blah on windows
279
329
let specPath = decodeURIComponent ( specUrl . pathname )
280
330
let resolvedPath = decodeURIComponent ( resolvedUrl . pathname )
@@ -288,13 +338,21 @@ function fromFile (res, where) {
288
338
if ( / ^ \/ ~ ( \/ | $ ) / . test ( specPath ) ) {
289
339
res . saveSpec = `file:${ specPath . substr ( 1 ) } `
290
340
resolvedPath = path . resolve ( homedir ( ) , specPath . substr ( 3 ) )
291
- } else if ( ! path . isAbsolute ( rawNoPrefix ) ) {
341
+ } else if ( ! path . isAbsolute ( rawSpec . slice ( 5 ) ) ) {
292
342
res . saveSpec = `file:${ path . relative ( where , resolvedPath ) } `
293
343
} else {
294
344
res . saveSpec = `file:${ path . resolve ( resolvedPath ) } `
295
345
}
296
346
297
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
+ }
298
356
return res
299
357
}
300
358
@@ -416,3 +474,8 @@ function fromRegistry (res) {
416
474
}
417
475
return res
418
476
}
477
+
478
+ module . exports = npa
479
+ module . exports . resolve = resolve
480
+ module . exports . toPurl = toPurl
481
+ module . exports . Result = Result
0 commit comments