Skip to content

Commit

Permalink
Add project files
Browse files Browse the repository at this point in the history
  • Loading branch information
Suneil Nyamathi committed Jun 13, 2017
1 parent 09b4327 commit 70164ad
Show file tree
Hide file tree
Showing 7 changed files with 337 additions and 0 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
artifacts
7 changes: 7 additions & 0 deletions .eslintrc
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
*.log
*.tgz
.nyc_output
artifacts
node_modules
8 changes: 8 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
*.log
*.tgz
.eslintignore
.eslintrc
.npmignore
.nyc_output
artifacts
tests
29 changes: 29 additions & 0 deletions package.json
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"
}
125 changes: 125 additions & 0 deletions semver-intersect.js
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;
162 changes: 162 additions & 0 deletions tests/unit/semver-intersect.js
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]);
});
});

0 comments on commit 70164ad

Please sign in to comment.