-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Suneil Nyamathi
committed
Jun 13, 2017
1 parent
09b4327
commit 70164ad
Showing
7 changed files
with
337 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
artifacts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
extends: eslint:recommended | ||
env: | ||
es6: true | ||
mocha: true | ||
node: true | ||
rules: | ||
semi: 2 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
*.log | ||
*.tgz | ||
.nyc_output | ||
artifacts | ||
node_modules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
*.log | ||
*.tgz | ||
.eslintignore | ||
.eslintrc | ||
.npmignore | ||
.nyc_output | ||
artifacts | ||
tests |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
{ | ||
"author": "Suneil Nyamathi <snyamathi@gmail.com>", | ||
"bugs": "http://github.com/snyamathi/semver-intersect/issues", | ||
"contributors": [], | ||
"dependencies": { | ||
"semver": "^5.0.0" | ||
}, | ||
"description": "Get the intersection of multiple semver ranges", | ||
"devDependencies": { | ||
"chai": "^4.0.0", | ||
"eslint": "^4.0.0", | ||
"jenkins-mocha": "^4.0.0" | ||
}, | ||
"keywords": [ | ||
"semver" | ||
], | ||
"license": "MIT", | ||
"main": "semver-intersect.js", | ||
"name": "semver-intersect", | ||
"repository": { | ||
"type": "git", | ||
"url": "git@github.com/snyamathi/semver-intersect" | ||
}, | ||
"scripts": { | ||
"lint": "eslint .", | ||
"test": "jenkins-mocha tests/unit --recursive" | ||
}, | ||
"version": "1.0.0" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
const semver = require('semver'); | ||
const regex = { | ||
condition: /^([<=>]+)?/, | ||
majorVersion: /\d+/, | ||
minMax: /^>=([\d]+\.[\d]+\.[\d]+(?:-[\w.]+)?) <([\d]+\.[\d]+\.[\d]+)$/, | ||
version: /([\d]+\.[\d]+\.[\d]+(?:-[\w.]+)?)$/, | ||
whitespace: /\s+/ | ||
}; | ||
|
||
function createShorthand (range) { | ||
const match = regex.minMax.exec(range); | ||
if (!match) { | ||
return range; | ||
} | ||
|
||
const [ min, max ] = match.slice(1); | ||
if (semver.major(min) !== semver.major(max)) { | ||
return `^${min}`; | ||
} | ||
|
||
return `~${min}`; | ||
} | ||
|
||
function ensureCompatible(range, ...bounds) { | ||
const { version } = parseRange(range); | ||
bounds.forEach(bound => { | ||
if (bound && !semver.satisfies(version, bound)) { | ||
throw new Error(`Range ${range} is not compatible with ${bound}`); | ||
} | ||
}); | ||
} | ||
|
||
function expandRanges (...ranges) { | ||
return ranges.reduce((result, range) => { | ||
const validRange = semver.validRange(range); | ||
const validRanges = validRange.split(regex.whitespace); | ||
return union(result, validRanges); | ||
}, []); | ||
} | ||
|
||
function formatIntersection ({ lowerBound = '', upperBound = '' }) { | ||
if (lowerBound === upperBound) { | ||
return lowerBound; | ||
} | ||
|
||
return `${lowerBound} ${upperBound}`.trim(); | ||
} | ||
|
||
function intersect (...ranges) { | ||
ranges = expandRanges(...ranges); | ||
|
||
const bounds = ranges.reduce(({ lowerBound, upperBound }, range) => { | ||
const { condition } = parseRange(range); | ||
|
||
// Exact version number specified, must be compatible with both bounds | ||
if (condition === '=') { | ||
ensureCompatible(range, lowerBound, upperBound); | ||
lowerBound = upperBound = range; | ||
} | ||
|
||
// New lower bound must be less than existing upper bound | ||
if (condition.startsWith('>')) { | ||
ensureCompatible(range, upperBound); | ||
lowerBound = mergeBounds(range, lowerBound); | ||
} | ||
|
||
// And vice versa | ||
if (condition.startsWith('<')) { | ||
ensureCompatible(range, lowerBound); | ||
upperBound = mergeBounds(range, upperBound); | ||
} | ||
|
||
return { lowerBound, upperBound }; | ||
}, {}); | ||
|
||
const range = formatIntersection(bounds); | ||
const shorthand = createShorthand(range); | ||
|
||
return shorthand; | ||
} | ||
|
||
function mergeBounds (range, bound) { | ||
if (!bound) { | ||
return range; | ||
} | ||
|
||
const { condition, version } = parseRange(range); | ||
const boundingVersion = parseRange(bound).version; | ||
const comparator = condition.startsWith('<') ? semver.lt : semver.gt; | ||
const strict = condition === '<' || condition === '>'; | ||
|
||
if (comparator(version, boundingVersion)) { | ||
return range; | ||
} else if (strict && semver.eq(version, boundingVersion)) { | ||
return range; | ||
} else { | ||
return bound; | ||
} | ||
} | ||
|
||
function parseRange (range) { | ||
const condition = regex.condition.exec(range)[1] || '='; | ||
const version = regex.version.exec(range)[1]; | ||
return { condition, version }; | ||
} | ||
|
||
function union (a, b) { | ||
return b.reduce((result, value) => { | ||
if (!result.includes(value)) { | ||
result.push(value); | ||
} | ||
return result; | ||
}, a); | ||
} | ||
|
||
module.exports.default = intersect; | ||
|
||
module.exports.createShorthand = createShorthand; | ||
module.exports.ensureCompatible = ensureCompatible; | ||
module.exports.expandRanges = expandRanges; | ||
module.exports.formatIntersection = formatIntersection; | ||
module.exports.intersect = intersect; | ||
module.exports.mergeBounds = mergeBounds; | ||
module.exports.parseRange = parseRange; | ||
module.exports.union = union; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
const expect = require('chai').expect; | ||
const { | ||
createShorthand, | ||
ensureCompatible, | ||
expandRanges, | ||
formatIntersection, | ||
intersect, | ||
mergeBounds, | ||
parseRange, | ||
union | ||
} = require('../../semver-intersect'); | ||
|
||
describe('createShorthand', () => { | ||
it('should return exact ranges', () => { | ||
const result = createShorthand('4.0.0'); | ||
expect(result).to.equal('4.0.0'); | ||
}); | ||
it('should simplify to a caret range', () => { | ||
const result = createShorthand('>=4.0.0 <5.0.0'); | ||
expect(result).to.equal('^4.0.0'); | ||
}); | ||
it('should simplify to a tilde range', () => { | ||
const result = createShorthand('>=4.0.0 <4.1.0'); | ||
expect(result).to.equal('~4.0.0'); | ||
}); | ||
it('should simplify with a pre-release tag', () => { | ||
const result = createShorthand('>=4.0.0-alpha <5.0.0'); | ||
expect(result).to.equal('^4.0.0-alpha'); | ||
}); | ||
it('should simplify with a pre-release tag and identifier', () => { | ||
const result = createShorthand('>=4.0.0-beta.1 <4.1.0'); | ||
expect(result).to.equal('~4.0.0-beta.1'); | ||
}); | ||
it('should return granular ranges without changes', () => { | ||
[ | ||
'>4.0.0', | ||
'<4.0.0', | ||
'<=4.0.0', | ||
'>=4.0.0', | ||
'4.0.0 - 4.0.5', | ||
'>4.0.0 <4.9.1' | ||
].forEach(range => { | ||
const result = createShorthand(range); | ||
expect(result).to.equal(range); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('ensureCompatible', () => { | ||
it('should throw if an lower bound is higher than the existing higher bound', () => { | ||
const call = ensureCompatible.bind(null, '>=3.0.0', '<2.0.0'); | ||
expect(call).to.throw('Range >=3.0.0 is not compatible with <2.0.0'); | ||
}); | ||
it('should throw if an upper bound is lower than the existing lower bound', () => { | ||
const call = ensureCompatible.bind(null, '<1.0.0', '>=2.0.0'); | ||
expect(call).to.throw('Range <1.0.0 is not compatible with >=2.0.0'); | ||
}); | ||
it('should throw if an exact range is above an existing upper bound', () => { | ||
const call = ensureCompatible.bind(null, '3.0.0', '<2.0.0'); | ||
expect(call).to.throw('Range 3.0.0 is not compatible with <2.0.0'); | ||
}); | ||
it('should throw if an exact range is below an existing lower bound', () => { | ||
const call = ensureCompatible.bind(null, '2.0.0', '>=3.0.0'); | ||
expect(call).to.throw('Range 2.0.0 is not compatible with >=3.0.0'); | ||
}); | ||
it('should not throw if the new range is compatible with existing bounds', () => { | ||
expect(ensureCompatible.bind(null, '2.0.0', '>=1.0.0')).to.not.throw(); | ||
expect(ensureCompatible.bind(null, '2.0.0')).to.not.throw(); | ||
expect(ensureCompatible.bind(null, '>=2.0.0', '<5.0.0')).to.not.throw(); | ||
expect(ensureCompatible.bind(null, '<4.0.0', '>2.0.0')).to.not.throw(); | ||
}); | ||
}); | ||
|
||
describe('expandRanges', () => { | ||
it('should expand the list of ranges into a set of unique individual ranges', () => { | ||
const result = expandRanges('>=3.0.0 <4.0.0', '>=3.1.0 <4.0.0', '>=3.3.0'); | ||
expect(result).to.deep.equal(['>=3.0.0', '<4.0.0', '>=3.1.0', '>=3.3.0']); | ||
}); | ||
}); | ||
|
||
describe('formatIntersection', () => { | ||
it('should return the exact condition if both bounds are the same', () => { | ||
const lowerBound = '3.0.0'; | ||
const upperBound = '3.0.0'; | ||
expect(formatIntersection({ lowerBound, upperBound })).to.equal('3.0.0'); | ||
}); | ||
it('should return the single condition if only one is provided', () => { | ||
const lowerBound = '>=3.0.0'; | ||
const upperBound = '<4.0.0'; | ||
expect(formatIntersection({ lowerBound })).to.equal('>=3.0.0'); | ||
expect(formatIntersection({ upperBound })).to.equal('<4.0.0'); | ||
}); | ||
it('should the two bounds separated by a space', () => { | ||
const lowerBound = '>=3.0.0'; | ||
const upperBound = '<4.0.0'; | ||
expect(formatIntersection({ lowerBound, upperBound })).to.equal('>=3.0.0 <4.0.0'); | ||
}); | ||
}); | ||
|
||
describe('intersect', () => { | ||
it('should return exact ranges', () => { | ||
const result = intersect('4.0.0'); | ||
expect(result).to.equal('4.0.0'); | ||
}); | ||
it('should return a caret range', () => { | ||
const result = intersect('^4.0.0', '^4.3.0'); | ||
expect(result).to.equal('^4.3.0'); | ||
}); | ||
it('should return a tilde range', () => { | ||
const result = intersect('^4.0.0', '~4.3.0'); | ||
expect(result).to.equal('~4.3.0'); | ||
}); | ||
it('should simplify redundant ranges', () => { | ||
const result = intersect('^4.0.0', '~4.3.89', '~4.3.24', '~4.3.63'); | ||
expect(result).to.equal('~4.3.89'); | ||
}); | ||
it('should throw on incompatible ranges', () => { | ||
const call = intersect.bind(null, '^4.0.0', '~4.3.0', '^4.4.0'); | ||
expect(call).to.throw('Range >=4.4.0 is not compatible with <4.4.0'); | ||
}); | ||
}); | ||
|
||
describe('mergeBounds', () => { | ||
it('should return the range if there are no existing bound', () => { | ||
expect(mergeBounds('>=5.0.0')).to.equal('>=5.0.0'); | ||
}); | ||
it('should merge a new lower bound with an existing lower bound', () => { | ||
expect(mergeBounds('>=5.0.0', '>=5.1.0')).to.equal('>=5.1.0'); | ||
expect(mergeBounds('>=5.5.0', '>=5.1.0')).to.equal('>=5.5.0'); | ||
}); | ||
it('should merge a new upper bound with an existing upper bound', () => { | ||
expect(mergeBounds('<4.2.0', '<4.1.0')).to.equal('<4.1.0'); | ||
expect(mergeBounds('<4.0.5', '<4.1.0')).to.equal('<4.0.5'); | ||
}); | ||
it('should replace a loose comparator with a strict comparator', () => { | ||
expect(mergeBounds('<4.1.0', '<=4.1.0')).to.equal('<4.1.0'); | ||
}); | ||
}); | ||
|
||
describe('parseRange', () => { | ||
it('return the comparison condition and version', () => { | ||
expect(parseRange('<5.0.0')).to.deep.equal({ | ||
condition: '<', | ||
version: '5.0.0' | ||
}); | ||
expect(parseRange('>=4.0.0')).to.deep.equal({ | ||
condition: '>=', | ||
version: '4.0.0' | ||
}); | ||
expect(parseRange('3.0.0')).to.deep.equal({ | ||
condition: '=', | ||
version: '3.0.0' | ||
}); | ||
}); | ||
}); | ||
|
||
describe('union', () => { | ||
it('should merge arrays without duplicates', () => { | ||
const result = union([1, 2, 3], [3, 4, 5]); | ||
expect(result).to.deep.equal([1, 2, 3, 4, 5]); | ||
}); | ||
}); |