Skip to content

Commit ab15ea4

Browse files
committed
fixup! windows paths and a bunch of encoding nonsense
1 parent 6e6f8e9 commit ab15ea4

File tree

3 files changed

+25
-39
lines changed

3 files changed

+25
-39
lines changed

README.md

+17-9
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,22 @@ Parses package name and specifier passed to commands like `npm install` or
88
## EXAMPLES
99

1010
```javascript
11-
var assert = require("assert")
12-
var npa = require("npm-package-arg")
11+
const assert = require("assert")
12+
const npa = require("npm-package-arg")
1313

1414
// Pass in the descriptor, and it'll return an object
1515
try {
16-
var parsed = npa("@bar/[email protected]")
16+
const parsed = npa("@bar/[email protected]")
1717
} catch (ex) {
1818
1919
}
2020
```
2121

2222
## USING
2323

24-
`var npa = require('npm-package-arg')`
24+
`const npa = require('npm-package-arg')`
2525

26-
### var result = npa(*arg*[, *where*])
26+
### const result = npa(*arg*[, *where*])
2727

2828
* *arg* - a string that you might pass to `npm install`, like:
2929
`[email protected]`, `@bar/[email protected]`, `foo@user/foo`, `http://x.com/foo.tgz`,
@@ -34,7 +34,7 @@ part, eg `foo` then the specifier will default to `latest`.
3434

3535
**Throws** if the package name is invalid, a dist-tag is invalid or a URL's protocol is not supported.
3636

37-
### var result = npa.resolve(*name*, *spec*[, *where*])
37+
### const result = npa.resolve(*name*, *spec*[, *where*])
3838

3939
* *name* - The name of the module you want to install. For example: `foo` or `@bar/foo`.
4040
* *spec* - The specifier indicating where and how you can get this module. Something like:
@@ -45,7 +45,7 @@ included then the default is `latest`.
4545

4646
**Throws** if the package name is invalid, a dist-tag is invalid or a URL's protocol is not supported.
4747

48-
### var purl = npa.toPurl(*arg*, *reg*)
48+
### const purl = npa.toPurl(*arg*, *reg*)
4949

5050
Returns the [purl (package URL)](https://github.com/package-url/purl-spec) form of the given package name/spec.
5151

@@ -79,9 +79,9 @@ keys:
7979
specification. Mostly used when making requests against a registry. When
8080
`name` is `null`, `escapedName` will also be `null`.
8181
* `rawSpec` - The specifier part that was parsed out in calls to `npa(arg)`,
82-
or the value of `spec` in calls to `npa.resolve(name, spec).
82+
or the value of `spec` in calls to `npa.resolve(name, spec)`.
8383
* `saveSpec` - The normalized specifier, for saving to package.json files.
84-
`null` for registry dependencies.
84+
`null` for registry dependencies. See note below about how this is (not) encoded.
8585
* `fetchSpec` - The version of the specifier to be used to fetch this
8686
resource. `null` for shortcuts to hosted git dependencies as there isn't
8787
just one URL to try with them.
@@ -94,3 +94,11 @@ keys:
9494
`npa.resolve(name, spec)` then this will be `name + '@' + spec`.
9595
* `subSpec` - If `type === 'alias'`, this is a Result Object for parsing the
9696
target specifier for the alias.
97+
98+
## SAVE SPECS
99+
100+
TLDR: `file:` urls are NOT uri encoded.
101+
102+
Historically, npm would uri decode file package args, but did not do any uri encoding for the `saveSpec`. This meant that it generated incorrect saveSpecs for directories with characters that *looked* like encoded uri characters, and also that it could not parse directories with some unencoded uri characters (such as `%`).
103+
104+
In order to fix this, and to not break all existing versions of npm, this module now parses all file package args as not being uri encoded. And in order to not break all of the package.json files npm has made in the past, it also does not uri encode the saveSpec. This includes package args that start with `file:`. This does mean that npm `file:` package args are not RFC compliant, and making them so constitutes quite a breaking change.

lib/npa.js

+8-27
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
const isWindows = process.platform === 'win32'
44

55
const { URL } = require('node:url')
6-
const path = isWindows ? require('node:path').win32 : require('node:path')
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')
78
const { homedir } = require('node:os')
89
const HostedGit = require('hosted-git-info')
910
const semver = require('semver')
@@ -276,39 +277,17 @@ function pathToFileURL (str) {
276277
for (let i = 0; i < str.length; i++) {
277278
result = `${result}${encodedPathChars.get(str[i]) ?? str[i]}`
278279
}
280+
if (result.startsWith('file:')) {
281+
return result
282+
}
279283
return `file:${result}`
280284
}
281285

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-
*/
304286
function fromFile (res, where) {
305287
res.type = isFileType.test(res.rawSpec) ? 'file' : 'directory'
306288
res.where = where
307289

308-
let rawSpec = res.rawSpec
309-
if (!rawSpec.startsWith('file:')) {
310-
rawSpec = pathToFileURL(rawSpec)
311-
}
290+
let rawSpec = pathToFileURL(res.rawSpec)
312291

313292
if (rawSpec.startsWith('file:/')) {
314293
// XXX backwards compatibility lack of compliance with RFC 8089
@@ -361,6 +340,8 @@ function fromFile (res, where) {
361340
}
362341

363342
res.fetchSpec = path.resolve(where, resolvedPath)
343+
// re-normalize the slashes in saveSpec due to node:path/win32 behavior in windows
344+
res.saveSpec = res.saveSpec.split('\\').join('/')
364345
return res
365346
}
366347

test/basic.js

-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ const normalizePath = p => p && p.replace(/^[a-zA-Z]:/, '').replace(/\\/g, '/')
66
const cwd = normalizePath(process.cwd())
77
process.cwd = () => cwd
88
const normalizePaths = spec => {
9-
spec.saveSpec = normalizePath(spec.saveSpec)
109
spec.fetchSpec = normalizePath(spec.fetchSpec)
1110
return spec
1211
}
@@ -15,8 +14,6 @@ const t = require('tap')
1514
const npa = t.mock('..', { path })
1615

1716
t.test('basic', function (t) {
18-
t.setMaxListeners(999)
19-
2017
const tests = {
2118
2219
name: 'foo',

0 commit comments

Comments
 (0)