Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ _API generated with [dmd-readme-api](https://www.npmjs.com/package/dmd-readme-ap
- [`plainFloatRe`](#plainFloatRe): Matches a plain (non-scientific notation) float.
- [`scientificFloatRe`](#scientificFloatRe): Matches a scientific notation float.
- <span id="global-constant-semver-index"></span>_semver_
- [`semver2RangeRe`](#semver2RangeRe): Matches a semantic versioning range specification.
- [`semver2Re`](#semver2Re): Matches a semantic version string according to the Semantic Versioning 2.0.0 specification.
- [`testCaptureGroups`](#testCaptureGroups): Tests that a regular expression correctly extracts capture groups from input strings.
- <span id="global-constant-URL-index"></span>_URL_
Expand Down Expand Up @@ -536,6 +537,14 @@ Matches a plain (non-scientific notation) float.

Matches a scientific notation float.

<a id="semver2RangeRe"></a>
### `semver2RangeRe` <sup>↱<sup>[source code](./src/semver.mjs#L99)</sup></sup> <sup>⇧<sup>[semver index](#global-constant-semver-index) | [global index](#global-constant-index)</sup></sup>

Matches a semantic versioning range specification. Allows for optional 'v' prefix (equivalent to '='), and otherwise
follows the [original spec's BNF grammar](https://docs.npmjs.com/cli/v6/using-npm/semver#range-grammar). This means
that an 'and' space between versions must be a single space and also requires exactly one space around hyphenated
ranges.

<a id="semver2Re"></a>
### `semver2Re` <sup>↱<sup>[source code](./src/semver.mjs#L31)</sup></sup> <sup>⇧<sup>[semver index](#global-constant-semver-index) | [global index](#global-constant-index)</sup></sup>

Expand Down
20 changes: 20 additions & 0 deletions src/npm.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ limitations under the License.
*/

import { lockdownRe } from './lib/lockdown-re'
import { semver2RangeReString } from './semver'

export const npmPackageNameReString = '(@[a-z0-9-~][a-z0-9-._~]*/)?([a-z0-9-~][a-z0-9-._~]*)'

Expand All @@ -24,3 +25,22 @@ export const npmPackageNameReString = '(@[a-z0-9-~][a-z0-9-._~]*/)?([a-z0-9-~][a
* @category NPM
*/
export const npmPackageNameRe = lockdownRe(npmPackageNameReString)

const npmPackageTagReString = `^(?!${semver2RangeReString}$)`

/**
* Matches an NPM package tag. A tag can, in theory, be anything that cannot be confused with a semver range. Due to
* the requirements of RE construction, the RE string ends up being useless for partial matches so is *NOT* exported.
* @category NPM
*/
export const npmPackageTagRe = new RegExp(npmPackageTagReString)

// since a package spec is a semver or not a semver, any non-blank string is valid
export const npmPackageSpecReString = '.+'

/**
* Matches an NPM package specification. Note, because any string that cannot be confused with a semver is, in theory,
* a valid tag, this could be any string.
* @category NPM
*/
export const npmPackageSpecRe = lockdownRe(npmPackageSpecReString)
68 changes: 68 additions & 0 deletions src/semver.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,71 @@ export const semver2ReString = '(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:
* @category semver
*/
export const semver2Re = lockdownRe(semver2ReString)

// the regex is based on the original spec's BNF grammar
// nr ::= '0' | ['1'-'9'] ( ['0'-'9'] ) *
const nr = '(?:0|[1-9][0-9]*)'

// xr ::= 'x' | 'X' | '*' | nr
const xr = `(?:[xX*]|${nr})`

// part ::= nr | [-0-9A-Za-z]+
const part = `(?:${nr}|[-0-9A-Za-z]+)`

// parts ::= part ( '.' part ) *
const parts = `(?:${part})(?:[.](?:${part}))*`

// pre ::= parts
const pre = parts

// build ::= parts
const build = parts

// qualifier ::= ( '-' pre )? ( '+' build )?
const qualifier = `(?:-${pre})?(?:[+]${build})?`

// ---- Composed nonterminals ----

// partial ::= xr ( '.' xr ( '.' xr qualifier ? )? )?
// (Note: qualifier only allowed after the 3rd component)
const partial =
`${xr}(?:[.]${xr}(?:[.]${xr}${qualifier}?)?)?`

// primitive ::= ( '<' | '>' | '>=' | '<=' | '=' ) partial
// Order matters so >= / <= are tried before > / <.
const primitive = `(?:(?:>=|<=|>|<|=|v)${partial})`

// tilde ::= '~' partial
const tilde = `(?:~${partial})`

// caret ::= '^' partial
const caret = `(?:[\\^]${partial})`

// simpleRange ::= primitive | partial | tilde | caret
// (Order chosen so ~ and ^ don't get mistaken for other forms)
const simpleRange = `(?:${tilde}|${caret}|${primitive}|${partial})`

// hyphen ::= partial ' - ' partial
// EXACT single spaces around the hyphen per BNF.
const hyphenRange = `(?:${partial}[ ][-][ ]${partial})`

// range ::= hyphen | simple ( ' ' simple ) * | ''
// EXACT single spaces between chained simples.
// The empty alternative '' is represented by | at the end.
const range = `(?:${hyphenRange}|${simpleRange}(?:[ ](?:${simpleRange}))*)`

// range-set ::= range ( logical-or range ) *
// logical-or ::= ( ' ' ) * '||' ( ' ' ) *
// Zero-or-more literal spaces around the ||.
const rangeSet = `${range}(?:[ ]*[|][|][ ]*(?:${range}))*`

export const semver2RangeReString = rangeSet

/**
* Matches a semantic versioning range specification. Allows for optional 'v' prefix (equivalent to '='), and otherwise
* follows the [original spec's BNF grammar](https://docs.npmjs.com/cli/v6/using-npm/semver#range-grammar). This means
* that an 'and' space between versions must be a single space and also requires exactly one space around hyphenated
* ranges.
* @category semver
*/
export const semver2RangeRe = lockdownRe(semver2RangeReString)
31 changes: 29 additions & 2 deletions src/test/data/npm.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

export const validNPMPackageNames = ['ansi-escapes', 'foo.com', '@acme/foo']
export const validNpmPackageNames = ['ansi-escapes', 'foo.com', '@acme/foo']

export const invalidNPMPackageNames = ['excited!', '.start-with-a-peried', '@acme/!foo']
export const invalidNpmPackageNames = ['excited!', '.start-with-a-peried', '@acme/!foo']

export const npmPackageNameCaptureGroupInputs = [
'ansi-escapes',
Expand All @@ -29,3 +29,30 @@ export const npmPackageNameCaptureGroupMatches = [
['@acme/', 'foo'],
[undefined, 'foo.com'],
]

// NPM tags - anything that is NOT a valid semver range
export const validNpmPackageTags = [
'latest',
'next',
'beta',
'canary'
]

export const invalidNpmPackageTags = [
'1.2.3',
'^1.0.0',
'~2.1.0',
'>=1.0.0'
]

// NPM package specs - can be anything (tags or semver ranges)
export const validNpmPackageSpecs = [
'latest',
'next',
'beta',
'canary',
'1.2.3',
'^1.0.0',
'~2.1.0',
'>=1.0.0'
]
157 changes: 157 additions & 0 deletions src/test/data/semver.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,160 @@ export const semver2CaptureGroupMatches = [
['1', '0', '0', 'beta', 'exp.sha.5114f85'],
['2', '0', '0', 'rc.1', 'build.123'],
]

// Valid semver range specifications
export const validSemver2Range = [
// Exact versions
'0.0.0',
'0.0.1',
'0.1.0',
'1.2.3',
'10.20.30',
'999.999.999',
'1.2.3+build.1',
'1.2.3+exp.sha.5114f85',
'1.2.3-alpha.1+build.42',
'1.2.3-01',
'=1.2.3',

// Wildcards and X-ranges
'*',
'x',
'X',
'1.x',
'1.X',
'1.*',
'1.2.x',
'1.2.*',
'0.x',
'0.0.x',

// Tilde ranges
'~1',
'~1.2',
'~1.2.3',
'~0',
'~0.2',
'~0.2.3',
'~10.20.30',

// Caret ranges
'^1',
'^1.2',
'^1.2.3',
'^0',
'^0.2',
'^0.2.3',
'^0.0',
'^0.0.3',

// Inequalities (comparators)
'>0.0.0',
'>=1.2.3',
'<2.0.0',
'<=2.0.0',
'>=1.2.3-alpha.1',
'<1.3.0-0',

// Hyphen ranges
'1.2.3 - 2.3.4',
'1.2 - 2.3',
'1.2.3 - 2.3',
'1.2 - 2',
'0.1.0 - 0.2.5',
'1.2.3-alpha.1 - 1.2.3',

// Mixed partials
'1',
'1.2',
'0',
'0.0',
'2',
'2.5',
'v1',
'v1.2',

// AND (space-separated comparator sets)
'>=1.2.3 <2.0.0',
'>1.2.3 <=2.3.4',
'>=0.0.0 <1.0.0',
'>=1.2.3-alpha.1 <1.3.0',

// OR (||) unions
'^1.2.3 || ^2.0.0',
'~1.2.3 || >=2.0.0 <3.0.0',
'1.x || >=2.0.0 <2.1.0',
'>=0.0.0 <1.0.0 || >=2.0.0',

// Prerelease ranges and exacts
'1.2.3-alpha',
'1.2.3-alpha.1',
'1.2.3-rc.0',
'1.2.3-rc.1+build.7',
'>=1.2.3-alpha.1 <1.3.0',
'^1.2.3-alpha.1',
'1.2.3-alpha.1 - 1.2.4-0',

// Build metadata examples
'2.3.4+build',
'2.3.4+build.11.e0f985a',
'2.3.4-rc.1+build.11',

// Loose 'v' prefix (accepted by many tools)
'v1.2.3',
'v0.1.0',
'v1.2.3-alpha.1',
'v1.2.3+build.5',
]

// partials will match sub-parts of the range, so is a more limited set than invalid semver ranges
export const invalidSemver2RangePartials = [
'',
'01.2.3',
'1.2.3.4',
'1.02.3',
'1.2.03',
'a.b.c',
'1.2.-3',
'1.2.x.y',
'x.y.z',
'1.*.*.*',
'=>1.2.3',
'><1.2.3',
'~>1.2.3',
'1.2.3-alpha..1',
'1.2.3-alpha.',
'1.2.3-alpha!1',
'1.2.3+build..1',
'1.2.3+!build',
'v',
'v.1',
'v.1.2',
'latest',
'next',
'canary',
'git+https://example.com/repo.git#v1.2.3',
'~',
'^',
'>',
'>=',
'<',
'<=',
]

// Invalid semver range specifications
export const invalidSemver2Range = [
...invalidSemver2RangePartials,
'>=1.2.3, <2.0.0',
'1.2.3 -- 2.0.0',
'1.2.3 -',
'- 1.2.3',
'(>=1.2.3 <2.0.0)',
'>=1.2.3 <2.0.0', // the spec is picky about 'and' spaces
'|| ^1.2.3',
'^1.2.3 ||',
'^1.2.3 || || ^2.0.0',
'>= 1.2.3 < 2.0.0',
'>= 1.2.3 < 2.0 .0',
'>=1.2.3 < 2.0.0 ||',
]
18 changes: 13 additions & 5 deletions src/test/npm.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,20 @@ limitations under the License.
import { groupTest, groupTestPartial, testCaptureGroups } from './lib/test-lib'
import * as regex from '../npm'
import {
validNPMPackageNames,
invalidNPMPackageNames,
validNpmPackageNames,
invalidNpmPackageNames,
npmPackageNameCaptureGroupInputs,
npmPackageNameCaptureGroupMatches
npmPackageNameCaptureGroupMatches,
validNpmPackageTags,
invalidNpmPackageTags,
validNpmPackageSpecs
} from './data/npm'

groupTest(regex.npmPackageNameRe, validNPMPackageNames, invalidNPMPackageNames, 'NPM package names')
groupTestPartial(regex.npmPackageNameReString, validNPMPackageNames, invalidNPMPackageNames, 'NPM package names')
groupTest(regex.npmPackageNameRe, validNpmPackageNames, invalidNpmPackageNames, 'NPM package names')
groupTestPartial(regex.npmPackageNameReString, validNpmPackageNames, invalidNpmPackageNames, 'NPM package names')
testCaptureGroups(regex.npmPackageNameRe, npmPackageNameCaptureGroupInputs, npmPackageNameCaptureGroupMatches, 'NPM package name capture groups')

groupTest(regex.npmPackageTagRe, validNpmPackageTags, invalidNpmPackageTags, 'NPM package tags')
// Note: Partial matching not tested for tags since any non-semver substring matches

groupTest(regex.npmPackageSpecRe, validNpmPackageSpecs, [], 'NPM package specs')
8 changes: 7 additions & 1 deletion src/test/semver.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,15 @@ import {
validSemver2,
invalidSemver2,
semver2CaptureGroupInputs,
semver2CaptureGroupMatches
semver2CaptureGroupMatches,
validSemver2Range,
invalidSemver2Range,
invalidSemver2RangePartials
} from './data/semver'

groupTest(regex.semver2Re, validSemver2, invalidSemver2, 'semver2')
groupTestPartial(regex.semver2ReString, validSemver2, invalidSemver2, 'semver2')
testCaptureGroups(regex.semver2Re, semver2CaptureGroupInputs, semver2CaptureGroupMatches, 'semver2 capture groups')

groupTest(regex.semver2RangeRe, validSemver2Range, invalidSemver2Range, 'semver2Range')
groupTestPartial(regex.semver2RangeReString, validSemver2Range, invalidSemver2RangePartials, 'semver2Range')