npm-package-arg
Advanced tools
Comparing version 4.2.1 to 5.0.0
332
npa.js
@@ -1,155 +0,191 @@ | ||
var url = require('url') | ||
var assert = require('assert') | ||
var util = require('util') | ||
var semver = require('semver') | ||
var HostedGit = require('hosted-git-info') | ||
'use strict' | ||
module.exports = npa | ||
module.exports.resolve = resolve | ||
module.exports.Result = Result | ||
var isWindows = process.platform === 'win32' || global.FAKE_WINDOWS | ||
var slashRe = isWindows ? /\\|[/]/ : /[/]/ | ||
let url | ||
let HostedGit | ||
let semver | ||
let path | ||
let validatePackageName | ||
let osenv | ||
var parseName = /^(?:@([^/]+?)[/])?([^/]+?)$/ | ||
var nameAt = /^(@([^/]+?)[/])?([^/]+?)@/ | ||
var debug = util.debuglog | ||
? util.debuglog('npa') | ||
: /\bnpa\b/i.test(process.env.NODE_DEBUG || '') | ||
? function () { | ||
console.error('NPA: ' + util.format.apply(util, arguments).split('\n').join('\nNPA: ')) | ||
const isWindows = process.platform === 'win32' || global.FAKE_WINDOWS | ||
const hasSlashes = isWindows ? /\\|[/]/ : /[/]/ | ||
const isURL = /^(?:git[+])?[a-z]+:/i | ||
const isFilename = /[.](?:tgz|tar.gz|tar)$/i | ||
function npa (arg, where) { | ||
let name | ||
let spec | ||
const nameEndsAt = arg[0] === '@' ? arg.slice(1).indexOf('@') + 1 : arg.indexOf('@') | ||
const namePart = nameEndsAt > 0 ? arg.slice(0, nameEndsAt) : arg | ||
if (isURL.test(arg)) { | ||
spec = arg | ||
} else if (namePart[0] !== '@' && (hasSlashes.test(namePart) || isFilename.test(namePart))) { | ||
spec = arg | ||
} else if (nameEndsAt > 0) { | ||
name = namePart | ||
spec = arg.slice(nameEndsAt + 1) | ||
} else { | ||
if (!validatePackageName) validatePackageName = require('validate-npm-package-name') | ||
const valid = validatePackageName(arg) | ||
if (valid.validForOldPackages) { | ||
name = arg | ||
} else { | ||
spec = arg | ||
} | ||
: function () {} | ||
function validName (name) { | ||
if (!name) { | ||
debug('not a name %j', name) | ||
return false | ||
} | ||
var n = name.trim() | ||
if (!n || n.charAt(0) === '.' || | ||
!n.match(/^[a-zA-Z0-9]/) || | ||
n.match(/[/()&?#|<>@:%\s\\*'"!~`]/) || | ||
n.toLowerCase() === 'node_modules' || | ||
n !== encodeURIComponent(n) || | ||
n.toLowerCase() === 'favicon.ico') { | ||
debug('not a valid name %j', name) | ||
return false | ||
} | ||
return n | ||
return resolve(name, spec, where, arg) | ||
} | ||
function npa (arg) { | ||
assert.equal(typeof arg, 'string') | ||
arg = arg.trim() | ||
const isFilespec = isWindows ? /^(?:[.]|~[/]|[/\\]|[a-zA-Z]:)/ : /^(?:[.]|~[/]|[/]|[a-zA-Z]:)/ | ||
var res = new Result() | ||
res.raw = arg | ||
res.scope = null | ||
res.escapedName = null | ||
function resolve (name, spec, where, arg) { | ||
const res = new Result({ | ||
raw: arg, | ||
name: name, | ||
rawSpec: spec, | ||
fromArgument: arg != null | ||
}) | ||
// See if it's something like foo@... | ||
var nameparse = arg.match(nameAt) | ||
debug('nameparse', nameparse) | ||
if (nameparse && validName(nameparse[3]) && | ||
(!nameparse[2] || validName(nameparse[2]))) { | ||
res.name = (nameparse[1] || '') + nameparse[3] | ||
res.escapedName = escapeName(res.name) | ||
if (nameparse[2]) { | ||
res.scope = '@' + nameparse[2] | ||
} | ||
arg = arg.substr(nameparse[0].length) | ||
if (name) res.setName(name) | ||
if (spec && (isFilespec.test(spec) || /^file:/i.test(spec))) { | ||
return fromFile(res, where) | ||
} | ||
if (!HostedGit) HostedGit = require('hosted-git-info') | ||
const hosted = HostedGit.fromUrl(spec, {noGitPlus: true, noCommittish: true}) | ||
if (hosted) { | ||
return fromHostedGit(res, hosted) | ||
} else if (spec && isURL.test(spec)) { | ||
return fromURL(res) | ||
} else if (spec && (hasSlashes.test(spec) || isFilename.test(spec))) { | ||
return fromFile(res, where) | ||
} else { | ||
res.name = null | ||
return fromRegistry(res) | ||
} | ||
} | ||
res.rawSpec = arg | ||
res.spec = arg | ||
function invalidPackageName (name, valid) { | ||
const err = new Error(`Invalid package name "${name}": ${valid.errors.join('; ')}`) | ||
err.code = 'EINVALIDPACKAGENAME' | ||
return err | ||
} | ||
function invalidTagName (name) { | ||
const err = new Error(`Invalid tag name "${name}": Tags may not have any characters that encodeURIComponent encodes.`) | ||
err.code = 'EINVALIDTAGNAME' | ||
return err | ||
} | ||
var urlparse = url.parse(arg) | ||
debug('urlparse', urlparse) | ||
function Result (opts) { | ||
this.type = opts.type | ||
this.registry = opts.registry | ||
this.where = opts.where | ||
if (opts.raw == null) { | ||
this.raw = opts.name ? opts.name + '@' + opts.rawSpec : opts.rawSpec | ||
} else { | ||
this.raw = opts.raw | ||
} | ||
this.name = undefined | ||
this.escapedName = undefined | ||
this.scope = undefined | ||
this.rawSpec = opts.rawSpec == null ? '' : opts.rawSpec | ||
this.saveSpec = opts.saveSpec | ||
this.fetchSpec = opts.fetchSpec | ||
if (opts.name) this.setName(opts.name) | ||
this.gitRange = opts.gitRange | ||
this.gitCommittish = opts.gitCommittish | ||
this.hosted = opts.hosted | ||
} | ||
Result.prototype = {} | ||
// windows paths look like urls | ||
// don't be fooled! | ||
if (isWindows && urlparse && urlparse.protocol && | ||
urlparse.protocol.match(/^[a-zA-Z]:$/)) { | ||
debug('windows url-ish local path', urlparse) | ||
urlparse = {} | ||
Result.prototype.setName = function (name) { | ||
if (!validatePackageName) validatePackageName = require('validate-npm-package-name') | ||
const valid = validatePackageName(name) | ||
if (!valid.validForOldPackages) { | ||
throw invalidPackageName(name, valid) | ||
} | ||
this.name = name | ||
this.scope = name[0] === '@' ? name.slice(0, name.indexOf('/')) : undefined | ||
// scoped packages in couch must have slash url-encoded, e.g. @foo%2Fbar | ||
this.escapedName = name.replace('/', '%2f') | ||
return this | ||
} | ||
if (urlparse.protocol || HostedGit.fromUrl(arg)) { | ||
return parseUrl(res, arg, urlparse) | ||
Result.prototype.toJSON = function () { | ||
const result = Object.assign({}, this) | ||
delete result.hosted | ||
return result | ||
} | ||
function setGitCommittish (res, committish) { | ||
if (committish != null && committish.length >= 7 && committish.slice(0, 7) === 'semver:') { | ||
res.gitRange = decodeURIComponent(committish.slice(7)) | ||
res.gitCommittish = null | ||
} else if (committish == null || committish === '') { | ||
res.gitCommittish = 'master' | ||
} else { | ||
res.gitCommittish = committish | ||
} | ||
return res | ||
} | ||
// at this point, it's not a url, and not hosted | ||
// If it's a valid name, and doesn't already have a name, then assume | ||
// $name@"" range | ||
// | ||
// if it's got / chars in it, then assume that it's local. | ||
const isAbsolutePath = /^[/]|^[A-Za-z]:/ | ||
if (res.name) { | ||
if (arg === '') arg = 'latest' | ||
var version = semver.valid(arg, true) | ||
var range = semver.validRange(arg, true) | ||
// foo@... | ||
if (version) { | ||
res.spec = version | ||
res.type = 'version' | ||
} else if (range) { | ||
res.spec = range | ||
res.type = 'range' | ||
} else if (slashRe.test(arg)) { | ||
parseLocal(res, arg) | ||
} else { | ||
res.type = 'tag' | ||
res.spec = arg | ||
} | ||
function resolvePath (where, spec) { | ||
if (isAbsolutePath.test(spec)) return spec | ||
if (!path) path = require('path') | ||
return path.resolve(where, spec) | ||
} | ||
function isAbsolute (dir) { | ||
if (dir[0] === '/') return true | ||
if (/^[A-Za-z]:/.test(dir)) return true | ||
return false | ||
} | ||
function fromFile (res, where) { | ||
if (!where) where = process.cwd() | ||
res.type = isFilename.test(res.rawSpec) ? 'file' : 'directory' | ||
res.where = where | ||
const spec = res.rawSpec.replace(/\\/g, '/') | ||
.replace(/^file:[/]*([A-Za-z]:)/, '$1') // drive name paths on windows | ||
.replace(/^file:(?:[/]*([~./]))?/, '$1') | ||
if (/^~[/]/.test(spec)) { | ||
// this is needed for windows and for file:~/foo/bar | ||
if (!osenv) osenv = require('osenv') | ||
res.fetchSpec = resolvePath(osenv.home(), spec.slice(2)) | ||
res.saveSpec = 'file:' + spec | ||
} else { | ||
var p = arg.match(parseName) | ||
if (p && validName(p[2]) && | ||
(!p[1] || validName(p[1]))) { | ||
res.type = 'tag' | ||
res.spec = 'latest' | ||
res.rawSpec = '' | ||
res.name = arg | ||
res.escapedName = escapeName(res.name) | ||
if (p[1]) { | ||
res.scope = '@' + p[1] | ||
} | ||
res.fetchSpec = resolvePath(where, spec) | ||
if (isAbsolute(spec)) { | ||
res.saveSpec = 'file:' + spec | ||
} else { | ||
parseLocal(res, arg) | ||
if (!path) path = require('path') | ||
res.saveSpec = 'file:' + path.relative(where, res.fetchSpec) | ||
} | ||
} | ||
return res | ||
} | ||
function escapeName (name) { | ||
// scoped packages in couch must have slash url-encoded, e.g. @foo%2Fbar | ||
return name && name.replace('/', '%2f') | ||
function fromHostedGit (res, hosted) { | ||
res.type = 'git' | ||
res.hosted = hosted | ||
res.saveSpec = hosted.toString({noGitPlus: false, noCommittish: false}) | ||
res.fetchSpec = hosted.getDefaultRepresentation() === 'shortcut' ? null : hosted.toString() | ||
return setGitCommittish(res, hosted.committish) | ||
} | ||
function parseLocal (res, arg) { | ||
// turns out nearly every character is allowed in fs paths | ||
if (/\0/.test(arg)) { | ||
throw new Error('Invalid Path: ' + JSON.stringify(arg)) | ||
} | ||
res.type = 'local' | ||
res.spec = arg | ||
function unsupportedURLType (protocol, spec) { | ||
const err = new Error(`Unsupported URL Type "${protocol}": ${spec}`) | ||
err.code = 'EUNSUPPORTEDPROTOCOL' | ||
return err | ||
} | ||
function parseUrl (res, arg, urlparse) { | ||
var gitHost = HostedGit.fromUrl(arg) | ||
if (gitHost) { | ||
res.type = 'hosted' | ||
res.spec = gitHost.toString() | ||
res.hosted = { | ||
type: gitHost.type, | ||
ssh: gitHost.ssh(), | ||
sshUrl: gitHost.sshurl(), | ||
httpsUrl: gitHost.https(), | ||
gitUrl: gitHost.git(), | ||
shortcut: gitHost.shortcut(), | ||
directUrl: gitHost.file('package.json') | ||
} | ||
return res | ||
} | ||
function fromURL (res) { | ||
if (!url) url = require('url') | ||
const urlparse = url.parse(res.rawSpec) | ||
res.saveSpec = res.rawSpec | ||
// check the protocol, and then see if it's git or not | ||
@@ -165,3 +201,6 @@ switch (urlparse.protocol) { | ||
res.type = 'git' | ||
res.spec = arg.replace(/^git[+]/, '') | ||
setGitCommittish(res, urlparse.hash != null ? urlparse.hash.slice(1) : '') | ||
urlparse.protocol = urlparse.protocol.replace(/^git[+]/, '') | ||
delete urlparse.hash | ||
res.fetchSpec = url.format(urlparse) | ||
break | ||
@@ -172,18 +211,7 @@ | ||
res.type = 'remote' | ||
res.spec = arg | ||
res.fetchSpec = res.saveSpec | ||
break | ||
case 'file:': | ||
res.type = 'local' | ||
if (isWindows && arg.match(/^file:\/\/\/?[a-z]:/i)) { | ||
// Windows URIs usually parse all wrong, so we just take matters | ||
// into our own hands, in this case. | ||
res.spec = arg.replace(/^file:\/\/\/?/i, '') | ||
} else { | ||
res.spec = urlparse.pathname | ||
} | ||
break | ||
default: | ||
throw new Error('Unsupported URL Type: ' + arg) | ||
throw unsupportedURLType(urlparse.protocol, res.rawSpec) | ||
} | ||
@@ -194,9 +222,23 @@ | ||
function Result () { | ||
if (!(this instanceof Result)) return new Result() | ||
function fromRegistry (res) { | ||
res.registry = true | ||
const spec = res.rawSpec === '' ? 'latest' : res.rawSpec | ||
// no save spec for registry components as we save based on the fetched | ||
// version, not on the argument so this can't compute that. | ||
res.saveSpec = null | ||
res.fetchSpec = spec | ||
if (!semver) semver = require('semver') | ||
const version = semver.valid(spec, true) | ||
const range = semver.validRange(spec, true) | ||
if (version) { | ||
res.type = 'version' | ||
} else if (range) { | ||
res.type = 'range' | ||
} else { | ||
if (encodeURIComponent(spec) !== spec) { | ||
throw invalidTagName(spec) | ||
} | ||
res.type = 'tag' | ||
} | ||
return res | ||
} | ||
Result.prototype.name = null | ||
Result.prototype.type = null | ||
Result.prototype.spec = null | ||
Result.prototype.raw = null | ||
Result.prototype.hosted = null |
{ | ||
"name": "npm-package-arg", | ||
"version": "4.2.1", | ||
"version": "5.0.0", | ||
"description": "Parse the things that can be arguments to `npm install`", | ||
@@ -13,11 +13,13 @@ "main": "npa.js", | ||
"dependencies": { | ||
"hosted-git-info": "^2.1.5", | ||
"semver": "^5.1.0" | ||
"hosted-git-info": "^2.4.2", | ||
"osenv": "^0.1.4", | ||
"semver": "^5.1.0", | ||
"validate-npm-package-name": "^3.0.0" | ||
}, | ||
"devDependencies": { | ||
"standard": "^7.1.2", | ||
"tap": "^5.7.2" | ||
"standard": "9.0.2", | ||
"tap": "^10.3.0" | ||
}, | ||
"scripts": { | ||
"test": "standard && tap --coverage test/*.js" | ||
"test": "standard && tap -J --coverage test/*.js" | ||
}, | ||
@@ -24,0 +26,0 @@ "repository": { |
117
README.md
# npm-package-arg | ||
Parse package name and specifier passed to commands like `npm install` or | ||
`npm cache add`. This just parses the text given-- it's worth noting that | ||
`npm` has further logic it applies by looking at your disk to figure out | ||
what ambiguous specifiers are. If you want that logic, please see | ||
[realize-package-specifier]. | ||
Parses package name and specifier passed to commands like `npm install` or | ||
`npm cache add`, or as found in `package.json` dependency sections. | ||
[realize-package-specifier]: https://www.npmjs.org/package/realize-package-specifier | ||
Arguments look like: `foo@1.2`, `@bar/foo@1.2`, `foo@user/foo`, `http://x.com/foo.tgz`, | ||
`git+https://github.com/user/foo`, `bitbucket:user/foo`, `foo.tar.gz` or `bar` | ||
## EXAMPLES | ||
@@ -21,42 +13,7 @@ | ||
// Pass in the descriptor, and it'll return an object | ||
var parsed = npa("@bar/foo@1.2") | ||
// Returns an object like: | ||
{ | ||
raw: '@bar/foo@1.2', // what was passed in | ||
name: '@bar/foo', // the name of the package | ||
escapedName: '@bar%2ffoo', // the escaped name, for making requests against a registry | ||
scope: '@bar', // the scope of the package, or null | ||
type: 'range', // the type of specifier this is | ||
spec: '>=1.2.0 <1.3.0', // the expanded specifier | ||
rawSpec: '1.2' // the specifier as passed in | ||
} | ||
// Parsing urls pointing at hosted git services produces a variation: | ||
var parsed = npa("git+https://github.com/user/foo") | ||
// Returns an object like: | ||
{ | ||
raw: 'git+https://github.com/user/foo', | ||
scope: null, | ||
name: null, | ||
escapedName: null, | ||
rawSpec: 'git+https://github.com/user/foo', | ||
spec: 'user/foo', | ||
type: 'hosted', | ||
hosted: { | ||
type: 'github', | ||
ssh: 'git@github.com:user/foo.git', | ||
sshurl: 'git+ssh://git@github.com/user/foo.git', | ||
https: 'https://github.com/user/foo.git', | ||
directUrl: 'https://raw.githubusercontent.com/user/foo/master/package.json' | ||
} | ||
try { | ||
var parsed = npa("@bar/foo@1.2") | ||
} catch (ex) { | ||
… | ||
} | ||
// Completely unreasonable invalid garbage throws an error | ||
// Make sure you wrap this in a try/catch if you have not | ||
// already sanitized the inputs! | ||
assert.throws(function() { | ||
npa("this is not \0 a valid package name or url") | ||
}) | ||
``` | ||
@@ -68,9 +25,24 @@ | ||
* var result = npa(*arg*) | ||
### var result = npa(*arg*[, *where*]) | ||
Parses *arg* and returns a result object detailing what *arg* is. | ||
* *arg* - a string that you might pass to `npm install`, like: | ||
`foo@1.2`, `@bar/foo@1.2`, `foo@user/foo`, `http://x.com/foo.tgz`, | ||
`git+https://github.com/user/foo`, `bitbucket:user/foo`, `foo.tar.gz`, | ||
`../foo/bar/` or `bar`. If the *arg* you provide doesn't have a specifier | ||
part, eg `foo` then the specifier will default to `latest`. | ||
* *where* - Optionally the path to resolve file paths relative to. Defaults to `process.cwd()` | ||
*arg* -- a package descriptor, like: `foo@1.2`, or `foo@user/foo`, or | ||
`http://x.com/foo.tgz`, or `git+https://github.com/user/foo` | ||
**Throws** if the package name is invalid, a dist-tag is invalid or a URL's protocol is not supported. | ||
### var result = npa.resolve(*name*, *spec*[, *where*]) | ||
* *name* - The name of the module you want to install. For example: `foo` or `@bar/foo`. | ||
* *spec* - The specifier indicating where and how you can get this module. Something like: | ||
`1.2`, `^1.7.17`, `http://x.com/foo.tgz`, `git+https://github.com/user/foo`, | ||
`bitbucket:user/foo`, `file:foo.tar.gz` or `file:../foo/bar/`. If not | ||
included then the default is `latest`. | ||
* *where* - Optionally the path to resolve file paths relative to. Defaults to `process.cwd()` | ||
**Throws** if the package name is invalid, a dist-tag is invalid or a URL's protocol is not supported. | ||
## RESULT OBJECT | ||
@@ -81,24 +53,13 @@ | ||
* `name` - If known, the `name` field expected in the resulting pkg. | ||
* `type` - One of the following strings: | ||
* `git` - A git repo | ||
* `hosted` - A hosted project, from github, bitbucket or gitlab. Originally | ||
either a full url pointing at one of these services or a shorthand like | ||
`user/project` or `github:user/project` for github or `bitbucket:user/project` | ||
for bitbucket. | ||
* `tag` - A tagged version, like `"foo@latest"` | ||
* `version` - A specific version number, like `"foo@1.2.3"` | ||
* `range` - A version range, like `"foo@2.x"` | ||
* `local` - A local file or folder path | ||
* `file` - A local `.tar.gz`, `.tar` or `.tgz` file. | ||
* `directory` - A local directory. | ||
* `remote` - An http url (presumably to a tgz) | ||
* `spec` - The "thing". URL, the range, git repo, etc. | ||
* `hosted` - If type=hosted this will be an object with the following keys: | ||
* `type` - github, bitbucket or gitlab | ||
* `ssh` - The ssh path for this git repo | ||
* `sshUrl` - The ssh URL for this git repo | ||
* `httpsUrl` - The HTTPS URL for this git repo | ||
* `directUrl` - The URL for the package.json in this git repo | ||
* `raw` - The original un-modified string that was provided. | ||
* `rawSpec` - The part after the `name@...`, as it was originally | ||
provided. | ||
* `registry` - If true this specifier refers to a resource hosted on a | ||
registry. This is true for `tag`, `version` and `range` types. | ||
* `name` - If known, the `name` field expected in the resulting pkg. | ||
* `scope` - If a name is something like `@org/module` then the `scope` | ||
@@ -110,5 +71,15 @@ field will be set to `@org`. If it doesn't have a scoped name, then | ||
`name` is `null`, `escapedName` will also be `null`. | ||
If you only include a name and no specifier part, eg, `foo` or `foo@` then | ||
a default of `latest` will be used (as of 4.1.0). This is contrast with | ||
previous behavior where `*` was used. | ||
* `rawSpec` - The specifier part that was parsed out in calls to `npa(arg)`, | ||
or the value of `spec` in calls to `npa.resolve(name, spec). | ||
* `saveSpec` - The normalized specifier, for saving to package.json files. | ||
`null` for registry dependencies. | ||
* `fetchSpec` - The version of the specifier to be used to fetch this | ||
resource. `null` for shortcuts to hosted git dependencies as there isn't | ||
just one URL to try with them. | ||
* `gitRange` - If set, this is a semver specifier to match against git tags with | ||
* `gitCommittish` - If set, this is the specific committish to use with a git dependency. | ||
* `hosted` - If `from === 'hosted'` then this will be a `hosted-git-info` | ||
object. This property is not included when serializing the object as | ||
JSON. | ||
* `raw` - The original un-modified string that was provided. If called as | ||
`npa.resolve(name, spec)` then this will be `name + '@' + spec`. |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
13137
5
217
0
4
82
+ Addedosenv@^0.1.4
+ Addedbuiltins@1.0.3(transitive)
+ Addedos-homedir@1.0.2(transitive)
+ Addedos-tmpdir@1.0.2(transitive)
+ Addedosenv@0.1.5(transitive)
+ Addedvalidate-npm-package-name@3.0.0(transitive)
Updatedhosted-git-info@^2.4.2