From 3902f063c954e86041ff94f93005770d6013806b Mon Sep 17 00:00:00 2001 From: reggi Date: Tue, 13 Oct 2026 15:56:34 -0400 Subject: [PATCH 1/3] chore: expand range parsing option coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e40646ba-5ae5-433c-abed-2cdf7f08793f --- test/classes/range.js | 27 +- test/fixtures/range-parse.js | 485 +++++++++++++++++++++++++++++------ 2 files changed, 432 insertions(+), 80 deletions(-) diff --git a/test/classes/range.js b/test/classes/range.js index d012e50c..7161516d 100644 --- a/test/classes/range.js +++ b/test/classes/range.js @@ -24,14 +24,37 @@ test('range parsing', t => { if (expect === null) { t.throws(() => new Range(range, options), TypeError, `invalid range: ${range}`) } else { - t.equal(new Range(range, options).range || '*', expect, `${range} => ${expect}`) - t.equal(new Range(range, options).range, new Range(expect).range, + const parsed = new Range(range, options) + t.equal(parsed.range || '*', expect, `${range} => ${expect}`) + t.equal(parsed.range, + new Range(expect, { includePrerelease: parsed.includePrerelease }).range, 'parsing both yields same result') } t.end() })) }) +test('range parsing covers every option combination', t => { + const inputsByOptions = new Map() + for (const [range, , options] of rangeParse) { + const loose = !!(options && options.loose) + const includePrerelease = !!(options && options.includePrerelease) + const key = `${loose}:${includePrerelease}` + if (!inputsByOptions.has(key)) { + inputsByOptions.set(key, []) + } + inputsByOptions.get(key).push(range) + } + + const combinations = ['false:false', 'true:false', 'false:true', 'true:true'] + t.strictSame([...inputsByOptions.keys()], combinations, 'all four modes are present') + const defaultInputs = inputsByOptions.get('false:false') + for (const key of combinations.slice(1)) { + t.strictSame(inputsByOptions.get(key), defaultInputs, `${key} covers the same inputs`) + } + t.end() +}) + test('throw for empty comparator set, even in loose mode', t => { t.throws(() => new Range('sadf||asdf', { loose: true }), TypeError('Invalid SemVer Range: sadf||asdf')) diff --git a/test/fixtures/range-parse.js b/test/fixtures/range-parse.js index 9f42f696..1a01ff90 100644 --- a/test/fixtures/range-parse.js +++ b/test/fixtures/range-parse.js @@ -5,14 +5,13 @@ // '*' is the return value from functions.validRange(), but // new Range().range will be '' in those cases const { MAX_SAFE_INTEGER } = require('../../internal/constants') -module.exports = [ + +// Expected results without options; every row below runs in all four modes. +const ranges = [ ['1.0.0 - 2.0.0', '>=1.0.0 <=2.0.0'], - ['1.0.0 - 2.0.0', '>=1.0.0-0 <2.0.1-0', { includePrerelease: true }], ['1 - 2', '>=1.0.0 <3.0.0-0'], - ['1 - 2', '>=1.0.0-0 <3.0.0-0', { includePrerelease: true }], ['1.0 - 2.0', '>=1.0.0 <2.1.0-0'], - ['1.0 - 2.0', '>=1.0.0-0 <2.1.0-0', { includePrerelease: true }], - ['1.0.0', '1.0.0', { loose: false }], + ['1.0.0', '1.0.0'], ['>=*', '*'], ['', '*'], ['*', '*'], @@ -32,14 +31,12 @@ module.exports = [ ['< 2.0.0', '<2.0.0'], ['<\t2.0.0', '<2.0.0'], ['1.2.3-dev = 1.2.3-dev', '1.2.3-dev'], - ['1.2.3-dev= 1', null, { loose: true }], ['<\u00a02.0.0', '<2.0.0'], ['<\ufeff2.0.0', '<2.0.0'], ['1.2.3 = 1.2.3', '1.2.3'], ['== 1', null], - ['>v= 1.2.3', '1.2.3', { loose: true }], + ['>v= 1.2.3', null], ['> +foo 1.2.3', null], - ['> +foo 1.2.3', '1.2.3', { loose: true }], ['1 +a +b = 2', null], ['~ +build 1', '>=1.0.0 <2.0.0-0'], ['~> +build 1', '>=1.0.0 <2.0.0-0'], @@ -48,7 +45,6 @@ module.exports = [ ['= v1.2.3', '1.2.3'], ['1.2.3\t=\n1.2.3', '1.2.3'], ['>= 1.2 < 2', '>=1.2.0 <2.0.0-0'], - ['>= 1.2 < 2', '>=1.2.0-0 <2.0.0-0', { includePrerelease: true }], ['~ 1.2 >= 1.2.3', '>=1.2.0 <1.3.0-0 >=1.2.3'], ['^ 0.2.3 < 0.3.0', '>=0.2.3 <0.3.0-0 <0.3.0'], ['>= 1.x <= 2.X', '>=1.0.0 <3.0.0-0'], @@ -57,9 +53,7 @@ module.exports = [ ['1.2.3+dev = 1.2.3+build', '1.2.3'], ['>= 1 +build < 2', '>=1.0.0 <2.0.0-0'], ['1.2.3 +build = 1.2.3', null], - ['1.2.3 +build = 1.2.3', '1.2.3', { loose: true }], ['= 01.2.3', null], - ['= 01.2.3', '1.2.3', { loose: true }], ['1.2.3 = 1.2.3 || >= 2 < 3', '1.2.3||>=2.0.0 <3.0.0-0'], ['1.2.3+build - 2.0.0+build', '>=1.2.3 <=2.0.0'], ['>= 2 < 1', '>=2.0.0 <1.0.0-0'], @@ -68,9 +62,7 @@ module.exports = [ ['=<1.2.3', null], ['><1.2.3', null], ['> = 1.2.3', null], - ['> = 1.2.3', '1.2.3', { loose: true }], ['== 1.2.3', null], - ['== 1.2.3', '1.2.3', { loose: true }], ['1.2.3-v = 1.2.3-v', '1.2.3-v'], ['1.2.3-vv = 1.2.3-vv', '1.2.3-vv'], ['1.2.3-1.dev = 1.2.3-1.dev', '1.2.3-1.dev'], @@ -81,24 +73,21 @@ module.exports = [ ['1.2.3-dev\u00a0=\ufeff1.2.3-dev', '1.2.3-dev'], ['1.2.3-dev+build = 1.2.3-dev', '1.2.3-dev'], ['1.2.3-dev +build = 1.2.3-dev', null], - ['1.2.3-dev +build = 1.2.3-dev', '1.2.3-dev', { loose: true }], ['>=1.2.3-dev = 1.2.3-dev', '>=1.2.3-dev 1.2.3-dev'], ['<=1.2.3-dev = 1.2.3-dev', '<=1.2.3-dev 1.2.3-dev'], ['~1.2.3-dev = 1.2.3-dev', '>=1.2.3-dev <1.3.0-0 1.2.3-dev'], ['^1.2.3-dev = 1.2.3-dev', '>=1.2.3-dev <2.0.0-0 1.2.3-dev'], ['1.2.3-dev <= 1.2.3', '1.2.3-dev <=1.2.3'], ['1.2.3-dev = 1', '1.2.3-dev >=1.0.0 <2.0.0-0'], - ['1.2.3-dev = 1', '1.2.3-dev >=1.0.0-0 <2.0.0-0', { includePrerelease: true }], ['1.2.3-dev = x', '1.2.3-dev'], ['1.2.3-dev = *', '1.2.3-dev'], ['1.2.3-dev || = 2.0.0', '1.2.3-dev||2.0.0'], ['1.2.3-dev - 2.0.0-dev', '>=1.2.3-dev <=2.0.0-dev'], ['1.2.3-dev= 1', null], ['1.2.3-dev== 1', null], - ['1.2.3-dev== 1', '>=1.0.0 <2.0.0-0', { loose: true }], - ['1.2.3-dev= 1 || 2.0.0', '2.0.0', { loose: true }], - ['1.2.3-dev || 1.2.3-dev= x', '1.2.3-dev', { loose: true }], - ['1.2.3 || 1.2.3= x', '1.2.3', { loose: true }], + ['1.2.3-dev= 1 || 2.0.0', null], + ['1.2.3-dev || 1.2.3-dev= x', null], + ['1.2.3 || 1.2.3= x', null], ['>=0.1.97', '>=0.1.97'], ['0.1.20 || 1.2.4', '0.1.20||1.2.4'], ['>=0.2.3 || <0.0.1', '>=0.2.3||<0.0.1'], @@ -116,9 +105,9 @@ module.exports = [ ['x', '*'], ['vvv1', '>=1.0.0 <2.0.0-0'], ['>= vvv1', '>=1.0.0'], - ['vvv1.2.3', '1.2.3', { loose: true }], - ['===1.2.3', '1.2.3', { loose: true }], - ['v=1.2.3', '1.2.3', { loose: true }], + ['vvv1.2.3', null], + ['===1.2.3', null], + ['v=1.2.3', null], ['2.*.*', '>=2.0.0 <3.0.0-0'], ['1.2.*', '>=1.2.0 <1.3.0-0'], ['1.2.* || 2.*', '>=1.2.0 <1.3.0-0||>=2.0.0 <3.0.0-0'], @@ -131,18 +120,11 @@ module.exports = [ ['~> 1', '>=1.0.0 <2.0.0-0'], ['~1.0', '>=1.0.0 <1.1.0-0'], ['~ 1.0', '>=1.0.0 <1.1.0-0'], - // tilde with includePrerelease must lower-bound at -0 just like the - // equivalent x-range (e.g. ~1.2 is documented as the same as 1.2.x), - // so that prereleases such as 1.2.0-rc match. See #512. - ['~1', '>=1.0.0-0 <2.0.0-0', { includePrerelease: true }], - ['~1.x', '>=1.0.0-0 <2.0.0-0', { includePrerelease: true }], - ['~1.2', '>=1.2.0-0 <1.3.0-0', { includePrerelease: true }], - ['~1.2.x', '>=1.2.0-0 <1.3.0-0', { includePrerelease: true }], - ['~0.0', '<0.1.0-0', { includePrerelease: true }], - // a fully-specified tilde keeps its exact lower bound (matches caret), - // and an explicit prerelease is preserved as-is - ['~1.2.3', '>=1.2.3 <1.3.0-0', { includePrerelease: true }], - ['~1.2.3-beta.4', '>=1.2.3-beta.4 <1.3.0-0', { includePrerelease: true }], + ['~1.x', '>=1.0.0 <2.0.0-0'], + ['~1.2', '>=1.2.0 <1.3.0-0'], + ['~1.2.x', '>=1.2.0 <1.3.0-0'], + ['~1.2.3', '>=1.2.3 <1.3.0-0'], + ['~1.2.3-beta.4', '>=1.2.3-beta.4 <1.3.0-0'], ['^0', '<1.0.0-0'], ['^ 1', '>=1.0.0 <2.0.0-0'], ['^0.1', '>=0.1.0 <0.2.0-0'], @@ -159,9 +141,7 @@ module.exports = [ ['>= 1', '>=1.0.0'], ['<1.2', '<1.2.0-0'], ['< 1.2', '<1.2.0-0'], - ['>01.02.03', '>1.2.3', true], ['>01.02.03', null], - ['~1.2.3beta', '>=1.2.3-beta <1.3.0-0', { loose: true }], ['~1.2.3beta', null], ['^ 1.2 ^ 1', '>=1.2.0 <2.0.0-0 >=1.0.0'], ['1.2 - 3.4.5', '>=1.2.0 <=3.4.5'], @@ -174,52 +154,401 @@ module.exports = [ ['* 2.x', '<0.0.0-0'], ['>x 2.x || * || =09090', null], - ['>=09090', '>=9090.0.0', true], - ['>=09090-0', null, { includePrerelease: true }], - ['>=09090-0', null, { loose: true, includePrerelease: true }], + ['>=09090-0', null], [`^${MAX_SAFE_INTEGER}.0.0`, null], [`=${MAX_SAFE_INTEGER}.0.0`, `${MAX_SAFE_INTEGER}.0.0`], [`^${MAX_SAFE_INTEGER - 1}.0.0`, `>=${MAX_SAFE_INTEGER - 1}.0.0 <${MAX_SAFE_INTEGER}.0.0-0`], // x-ranges with build metadata - ['1.x.x+build >2.x+build', '>=1.0.0 <2.0.0-0 >=3.0.0', null], - ['>=1.x+build <2.x.x+build', '>=1.0.0 <2.0.0-0', null], - ['1.x.x+build || 2.x.x+build', '>=1.0.0 <2.0.0-0||>=2.0.0 <3.0.0-0', null], - ['1.x+build.123', '>=1.0.0 <2.0.0-0', null], - ['1.x.x+meta-data', '>=1.0.0 <2.0.0-0', null], - ['1.x.x+build.123 >2.x.x+meta-data', '>=1.0.0 <2.0.0-0 >=3.0.0', null], - ['1.x.x+build <2.x.x+meta', '>=1.0.0 <2.0.0-0', null], - ['>1.x+build <=2.x.x+meta', '>=2.0.0 <3.0.0-0', null], - [' 1.x.x+build >2.x.x+build ', '>=1.0.0 <2.0.0-0 >=3.0.0', null], - ['^1.x+build', '>=1.0.0 <2.0.0-0', null], - ['^1.x.x+build', '>=1.0.0 <2.0.0-0', null], - ['^1.2.x+build', '>=1.2.0 <2.0.0-0', null], - ['^1.x+meta-data', '>=1.0.0 <2.0.0-0', null], - ['^1.x.x+build.123', '>=1.0.0 <2.0.0-0', null], - ['~1.x+build', '>=1.0.0 <2.0.0-0', null], - ['~1.x.x+build', '>=1.0.0 <2.0.0-0', null], - ['~1.2.x+build', '>=1.2.0 <1.3.0-0', null], - ['~1.x+meta-data', '>=1.0.0 <2.0.0-0', null], - ['~1.x.x+build.123', '>=1.0.0 <2.0.0-0', null], - ['^1.x.x+build || ~2.x.x+meta', '>=1.0.0 <2.0.0-0||>=2.0.0 <3.0.0-0', null], - ['~1.x.x+build >2.x+meta', '>=1.0.0 <2.0.0-0 >=3.0.0', null], - ['^1.x+build.123 <2.x.x+meta-data', '>=1.0.0 <2.0.0-0', null], + ['1.x.x+build >2.x+build', '>=1.0.0 <2.0.0-0 >=3.0.0'], + ['>=1.x+build <2.x.x+build', '>=1.0.0 <2.0.0-0'], + ['1.x.x+build || 2.x.x+build', '>=1.0.0 <2.0.0-0||>=2.0.0 <3.0.0-0'], + ['1.x+build.123', '>=1.0.0 <2.0.0-0'], + ['1.x.x+meta-data', '>=1.0.0 <2.0.0-0'], + ['1.x.x+build.123 >2.x.x+meta-data', '>=1.0.0 <2.0.0-0 >=3.0.0'], + ['1.x.x+build <2.x.x+meta', '>=1.0.0 <2.0.0-0'], + ['>1.x+build <=2.x.x+meta', '>=2.0.0 <3.0.0-0'], + [' 1.x.x+build >2.x.x+build ', '>=1.0.0 <2.0.0-0 >=3.0.0'], + ['^1.x+build', '>=1.0.0 <2.0.0-0'], + ['^1.x.x+build', '>=1.0.0 <2.0.0-0'], + ['^1.2.x+build', '>=1.2.0 <2.0.0-0'], + ['^1.x+meta-data', '>=1.0.0 <2.0.0-0'], + ['^1.x.x+build.123', '>=1.0.0 <2.0.0-0'], + ['~1.x+build', '>=1.0.0 <2.0.0-0'], + ['~1.x.x+build', '>=1.0.0 <2.0.0-0'], + ['~1.2.x+build', '>=1.2.0 <1.3.0-0'], + ['~1.x+meta-data', '>=1.0.0 <2.0.0-0'], + ['~1.x.x+build.123', '>=1.0.0 <2.0.0-0'], + ['^1.x.x+build || ~2.x.x+meta', '>=1.0.0 <2.0.0-0||>=2.0.0 <3.0.0-0'], + ['~1.x.x+build >2.x+meta', '>=1.0.0 <2.0.0-0 >=3.0.0'], + ['^1.x+build.123 <2.x.x+meta-data', '>=1.0.0 <2.0.0-0'], // // x-ranges with prerelease and build - ['1.x.x-alpha+build', '>=1.0.0 <2.0.0-0', null], - ['>1.x.x-alpha+build', '>=2.0.0', null], - ['>=1.x.x-alpha+build <2.x.x+build', '>=1.0.0 <2.0.0-0', null], - ['1.x.x-alpha+build || 2.x.x+build', '>=1.0.0 <2.0.0-0||>=2.0.0 <3.0.0-0', null], + ['1.x.x-alpha+build', '>=1.0.0 <2.0.0-0'], + ['>1.x.x-alpha+build', '>=2.0.0'], + ['>=1.x.x-alpha+build <2.x.x+build', '>=1.0.0 <2.0.0-0'], + ['1.x.x-alpha+build || 2.x.x+build', '>=1.0.0 <2.0.0-0||>=2.0.0 <3.0.0-0'], // long build metadata must be stripped, not bled into the version - ['4.17.0+' + 'a'.repeat(250) + '3', '4.17.0', { loose: true }], - ['4.17.0+' + 'a'.repeat(251), '4.17.0', { loose: true }], - ['v1.0+' + 'a'.repeat(249) + 'x6', '>=1.0.0 <1.1.0-0', null], - ['1.2.3+' + 'a'.repeat(251) + ' - 2.0.0', '>=1.2.3 <=2.0.0', null], - ['1.2.3+' + 'a'.repeat(251) + ' - 2.0.0', '>=1.2.3 <=2.0.0', { loose: true }], - ['> 1.2.3+' + 'a'.repeat(251), '>1.2.3', null], - ['>= 1.2.3+' + 'a'.repeat(251), '>=1.2.3', { loose: true }], - ['~1.2.3+' + 'a'.repeat(251), '>=1.2.3 <1.3.0-0', null], - ['^1.2.3+' + 'a'.repeat(251), '>=1.2.3 <2.0.0-0', null], - ['1.2.3+sha512.' + 'a'.repeat(251), '1.2.3', { loose: true }], - ['1.2.3+sha256.' + 'a'.repeat(200) + '.' + 'b'.repeat(200), '1.2.3', { loose: true }], - ['1.2.3+' + 'a'.repeat(251) + ' || 2.0.0+' + 'b'.repeat(251), - '1.2.3||2.0.0', { loose: true }], + ['4.17.0+' + 'a'.repeat(250) + '3', '4.17.0'], + ['4.17.0+' + 'a'.repeat(251), '4.17.0'], + ['v1.0+' + 'a'.repeat(249) + 'x6', '>=1.0.0 <1.1.0-0'], + ['1.2.3+' + 'a'.repeat(251) + ' - 2.0.0', '>=1.2.3 <=2.0.0'], + ['> 1.2.3+' + 'a'.repeat(251), '>1.2.3'], + ['>= 1.2.3+' + 'a'.repeat(251), '>=1.2.3'], + ['~1.2.3+' + 'a'.repeat(251), '>=1.2.3 <1.3.0-0'], + ['^1.2.3+' + 'a'.repeat(251), '>=1.2.3 <2.0.0-0'], + ['1.2.3+sha512.' + 'a'.repeat(251), '1.2.3'], + ['1.2.3+sha256.' + 'a'.repeat(200) + '.' + 'b'.repeat(200), '1.2.3'], + ['1.2.3+' + 'a'.repeat(251) + ' || 2.0.0+' + 'b'.repeat(251), '1.2.3||2.0.0'], + // Each comparison operator at every version precision, with wildcard aliases. + ['1.2.3', '1.2.3'], + ['1', '>=1.0.0 <2.0.0-0'], + ['1.x', '>=1.0.0 <2.0.0-0'], + ['1.X', '>=1.0.0 <2.0.0-0'], + ['1.*', '>=1.0.0 <2.0.0-0'], + ['1.x.x', '>=1.0.0 <2.0.0-0'], + ['1.2', '>=1.2.0 <1.3.0-0'], + ['1.2.x', '>=1.2.0 <1.3.0-0'], + ['1.2.X', '>=1.2.0 <1.3.0-0'], + ['1.2.*', '>=1.2.0 <1.3.0-0'], + ['x', '*'], + ['X', '*'], + ['*', '*'], + ['x.x', '*'], + ['*.*.*', '*'], + ['=1.2.3', '1.2.3'], + ['=1', '>=1.0.0 <2.0.0-0'], + ['=1.x', '>=1.0.0 <2.0.0-0'], + ['=1.X', '>=1.0.0 <2.0.0-0'], + ['=1.*', '>=1.0.0 <2.0.0-0'], + ['=1.x.x', '>=1.0.0 <2.0.0-0'], + ['=1.2', '>=1.2.0 <1.3.0-0'], + ['=1.2.x', '>=1.2.0 <1.3.0-0'], + ['=1.2.X', '>=1.2.0 <1.3.0-0'], + ['=1.2.*', '>=1.2.0 <1.3.0-0'], + ['=x', '*'], + ['=X', '*'], + ['=*', '*'], + ['=x.x', '*'], + ['=*.*.*', '*'], + ['>1.2.3', '>1.2.3'], + ['>1', '>=2.0.0'], + ['>1.x', '>=2.0.0'], + ['>1.X', '>=2.0.0'], + ['>1.*', '>=2.0.0'], + ['>1.x.x', '>=2.0.0'], + ['>1.2', '>=1.3.0'], + ['>1.2.x', '>=1.3.0'], + ['>1.2.X', '>=1.3.0'], + ['>1.2.*', '>=1.3.0'], + ['>x', '<0.0.0-0'], + ['>X', '<0.0.0-0'], + ['>*', '<0.0.0-0'], + ['>x.x', '<0.0.0-0'], + ['>*.*.*', '<0.0.0-0'], + ['>=1.2.3', '>=1.2.3'], + ['>=1', '>=1.0.0'], + ['>=1.x', '>=1.0.0'], + ['>=1.X', '>=1.0.0'], + ['>=1.*', '>=1.0.0'], + ['>=1.x.x', '>=1.0.0'], + ['>=1.2', '>=1.2.0'], + ['>=1.2.x', '>=1.2.0'], + ['>=1.2.X', '>=1.2.0'], + ['>=1.2.*', '>=1.2.0'], + ['>=x', '*'], + ['>=X', '*'], + ['>=*', '*'], + ['>=x.x', '*'], + ['>=*.*.*', '*'], + ['<1.2.3', '<1.2.3'], + ['<1', '<1.0.0-0'], + ['<1.x', '<1.0.0-0'], + ['<1.X', '<1.0.0-0'], + ['<1.*', '<1.0.0-0'], + ['<1.x.x', '<1.0.0-0'], + ['<1.2', '<1.2.0-0'], + ['<1.2.x', '<1.2.0-0'], + ['<1.2.X', '<1.2.0-0'], + ['<1.2.*', '<1.2.0-0'], + ['=0.0.0', '*'], + ['>=0.0.0-0', '>=0.0.0-0'], + ['0', '<1.0.0-0'], + ['0.0', '<0.1.0-0'], + ['0.0.0', '0.0.0'], + ['<0', '<0.0.0-0'], + ['<=0.0', '<0.1.0-0'], + // Wildcard and zero-major tilde/caret branches, including explicit prereleases. + ['~*', '*'], + ['~>x', '*'], + ['^X', '*'], + ['~0', '<1.0.0-0'], + ['~0.0', '<0.1.0-0'], + ['~0.0.1', '>=0.0.1 <0.1.0-0'], + ['~0.2.3-beta.1', '>=0.2.3-beta.1 <0.3.0-0'], + ['^0.0', '<0.1.0-0'], + ['^0.0.x', '<0.1.0-0'], + ['^0.0.0', '<0.0.1-0'], + ['^0.1.2-beta.1', '>=0.1.2-beta.1 <0.2.0-0'], + // Open hyphen bounds and asymmetric partial/prerelease endpoints. + ['* - *', '*'], + ['* - 2.3.4', '<=2.3.4'], + ['1.2.3 - *', '>=1.2.3'], + ['1.x - 2.3.x', '>=1.0.0 <2.4.0-0'], + ['1.2 - 3', '>=1.2.0 <4.0.0-0'], + ['1.2.3-beta.1 - 2.3.4', '>=1.2.3-beta.1 <=2.3.4'], + ['1.2.3 - 2.3.4-beta.1', '>=1.2.3 <=2.3.4-beta.1'], + ['1.2\t-\n2.3', '>=1.2.0 <2.4.0-0'], + // AND deduplication, empty OR branches, and null-set/loose-mode filtering. + ['>=1.2.3 >=1.2.3 <2.0.0 <2.0.0', '>=1.2.3 <2.0.0'], + ['* >=1.2.3 *', '>=1.2.3'], + ['>=1.2.3 <* <2.0.0', '<0.0.0-0'], + ['<* || >=1.2.3', '>=1.2.3'], + ['>=1.2.3 || >*', '>=1.2.3'], + [' || 1.2.3', '*'], + ['1.2.3 || ', '*'], + ['||||', '*'], + ['1.2.3 || invalid', null], + ['invalid || 1.2.3', null], + ['invalid || invalid', null], + ['>=1.2.3 invalid <2.0.0', null], + // Malformed versions, separators, and strict versus loose numeric identifiers. + ['1.2.3-01', null], + ['1.2.3-01alpha', '1.2.3-01alpha'], + ['1.2.3-0', '1.2.3-0'], + ['1.2.3+00', '1.2.3'], + ['1.2.3-', null], + ['1.2.3-alpha..1', null], + ['1.2.3-alpha_1', null], + ['1.2.3+', null], + ['1.2.3+build..1', null], + ['1.2.3.4', null], + ['1.', null], + ['1.2.', null], + ['1.2.3, <2.0.0', null], + ['1.2.3 | 2.0.0', null], + ['>=', null], + ['~', null], + ['^', null], + ['1.2.3 -', null], + ['- 2.0.0', null], + ['1.2.3-2.0.0', '1.2.3-2.0.0'], + ['~01.02.03', null], + ['^01.02.03', null], + ['>=01.02 <02', null], + // Numeric limits apply independently to each component and generated bound. + [`1.${MAX_SAFE_INTEGER}.0`, `1.${MAX_SAFE_INTEGER}.0`], + [`1.2.${MAX_SAFE_INTEGER}`, `1.2.${MAX_SAFE_INTEGER}`], + [`${MAX_SAFE_INTEGER + 1}.0.0`, null], + [`1.${MAX_SAFE_INTEGER + 1}.0`, null], + [`1.2.${MAX_SAFE_INTEGER + 1}`, null], + [`~1.${MAX_SAFE_INTEGER}.0`, null], + [`^0.0.${MAX_SAFE_INTEGER}`, null], + [`>1.${MAX_SAFE_INTEGER}`, null], + [`<=${MAX_SAFE_INTEGER}`, null], + [`1.2.3 - 1.2.${MAX_SAFE_INTEGER}`, `>=1.2.3 <=1.2.${MAX_SAFE_INTEGER}`], +] + +// Only list results that differ from the default-options expectation. +const looseOverrides = new Map([ + ['== 1', '>=1.0.0 <2.0.0-0'], + ['>v= 1.2.3', '1.2.3'], + ['> +foo 1.2.3', '1.2.3'], + ['1 +a +b = 2', '>=1.0.0 <2.0.0-0 >=2.0.0 <3.0.0-0'], + ['1.2.3 +build = 1.2.3', '1.2.3'], + ['= 01.2.3', '1.2.3'], + ['> = 1.2.3', '1.2.3'], + ['== 1.2.3', '1.2.3'], + ['1.2.3-dev +build = 1.2.3-dev', '1.2.3-dev'], + ['1.2.3-dev== 1', '>=1.0.0 <2.0.0-0'], + ['1.2.3-dev= 1 || 2.0.0', '2.0.0'], + ['1.2.3-dev || 1.2.3-dev= x', '1.2.3-dev'], + ['1.2.3 || 1.2.3= x', '1.2.3'], + ['1.x.5 || 2.x', '>=2.0.0 <3.0.0-0'], + ['vvv1.2.3', '1.2.3'], + ['===1.2.3', '1.2.3'], + ['v=1.2.3', '1.2.3'], + ['>01.02.03', '>1.2.3'], + ['~1.2.3beta', '>=1.2.3-beta <1.3.0-0'], + ['>=09090', '>=9090.0.0'], + ['1.2.3 || invalid', '1.2.3'], + ['invalid || 1.2.3', '1.2.3'], + ['>=1.2.3 invalid <2.0.0', '>=1.2.3 <2.0.0'], + ['1.2.3-01', '1.2.3-1'], + ['1.2.3-', '1.2.3--'], + ['1.2.3, <2.0.0', '<2.0.0'], + ['1.2.3 | 2.0.0', '1.2.3 2.0.0'], + ['1.2.3 -', '1.2.3'], + ['- 2.0.0', '2.0.0'], + ['~01.02.03', '>=1.2.3 <1.3.0-0'], + ['^01.02.03', '>=1.2.3 <2.0.0-0'], + ['>=01.02 <02', '>=1.2.0 <2.0.0-0'], +]) + +// Tilde with includePrerelease must lower-bound at -0 just like the +// equivalent x-range (e.g. ~1.2 is documented as the same as 1.2.x), +// so that prereleases such as 1.2.0-rc match. See #512. +// A fully-specified tilde keeps its exact lower bound (matches caret), +// and an explicit prerelease is preserved as-is +const prereleaseOverrides = new Map([ + ['1.0.0 - 2.0.0', '>=1.0.0-0 <2.0.1-0'], + ['1 - 2', '>=1.0.0-0 <3.0.0-0'], + ['1.0 - 2.0', '>=1.0.0-0 <2.1.0-0'], + ['1', '>=1.0.0-0 <2.0.0-0'], + ['~ +build 1', '>=1.0.0-0 <2.0.0-0'], + ['~> +build 1', '>=1.0.0-0 <2.0.0-0'], + ['^ +build 1', '>=1.0.0-0 <2.0.0-0'], + ['>= 1.2 < 2', '>=1.2.0-0 <2.0.0-0'], + ['~ 1.2 >= 1.2.3', '>=1.2.0-0 <1.3.0-0 >=1.2.3'], + ['>= 1.x <= 2.X', '>=1.0.0-0 <3.0.0-0'], + ['>= 1 +build < 2', '>=1.0.0-0 <2.0.0-0'], + ['1.2.3 = 1.2.3 || >= 2 < 3', '1.2.3||>=2.0.0-0 <3.0.0-0'], + ['1.2.3+build - 2.0.0+build', '>=1.2.3-0 <2.0.1-0'], + ['>= 2 < 1', '>=2.0.0-0 <1.0.0-0'], + ['1.2.3-dev = 1', '1.2.3-dev >=1.0.0-0 <2.0.0-0'], + ['2.x.x', '>=2.0.0-0 <3.0.0-0'], + ['1.2.x', '>=1.2.0-0 <1.3.0-0'], + ['1.2.x || 2.x', '>=1.2.0-0 <1.3.0-0||>=2.0.0-0 <3.0.0-0'], + ['vvv1', '>=1.0.0-0 <2.0.0-0'], + ['>= vvv1', '>=1.0.0-0'], + ['2.*.*', '>=2.0.0-0 <3.0.0-0'], + ['1.2.*', '>=1.2.0-0 <1.3.0-0'], + ['1.2.* || 2.*', '>=1.2.0-0 <1.3.0-0||>=2.0.0-0 <3.0.0-0'], + ['2', '>=2.0.0-0 <3.0.0-0'], + ['2.3', '>=2.3.0-0 <2.4.0-0'], + ['~2.4', '>=2.4.0-0 <2.5.0-0'], + ['~1', '>=1.0.0-0 <2.0.0-0'], + ['~>1', '>=1.0.0-0 <2.0.0-0'], + ['~> 1', '>=1.0.0-0 <2.0.0-0'], + ['~1.0', '>=1.0.0-0 <1.1.0-0'], + ['~ 1.0', '>=1.0.0-0 <1.1.0-0'], + ['~1.x', '>=1.0.0-0 <2.0.0-0'], + ['~1.2', '>=1.2.0-0 <1.3.0-0'], + ['~1.2.x', '>=1.2.0-0 <1.3.0-0'], + ['^ 1', '>=1.0.0-0 <2.0.0-0'], + ['^0.1', '>=0.1.0-0 <0.2.0-0'], + ['^1.0', '>=1.0.0-0 <2.0.0-0'], + ['^1.2', '>=1.2.0-0 <2.0.0-0'], + ['>=1', '>=1.0.0-0'], + ['>= 1', '>=1.0.0-0'], + ['^ 1.2 ^ 1', '>=1.2.0-0 <2.0.0-0 >=1.0.0-0'], + ['1.2 - 3.4.5', '>=1.2.0-0 <3.4.6-0'], + ['1.2.3 - 3.4', '>=1.2.3-0 <3.5.0-0'], + ['1.2 - 3.4', '>=1.2.0-0 <3.5.0-0'], + ['>1', '>=2.0.0-0'], + ['>1.2', '>=1.3.0-0'], + ['1.x.x+build >2.x+build', '>=1.0.0-0 <2.0.0-0 >=3.0.0-0'], + ['>=1.x+build <2.x.x+build', '>=1.0.0-0 <2.0.0-0'], + ['1.x.x+build || 2.x.x+build', '>=1.0.0-0 <2.0.0-0||>=2.0.0-0 <3.0.0-0'], + ['1.x+build.123', '>=1.0.0-0 <2.0.0-0'], + ['1.x.x+meta-data', '>=1.0.0-0 <2.0.0-0'], + ['1.x.x+build.123 >2.x.x+meta-data', '>=1.0.0-0 <2.0.0-0 >=3.0.0-0'], + ['1.x.x+build <2.x.x+meta', '>=1.0.0-0 <2.0.0-0'], + ['>1.x+build <=2.x.x+meta', '>=2.0.0-0 <3.0.0-0'], + [' 1.x.x+build >2.x.x+build ', '>=1.0.0-0 <2.0.0-0 >=3.0.0-0'], + ['^1.x+build', '>=1.0.0-0 <2.0.0-0'], + ['^1.x.x+build', '>=1.0.0-0 <2.0.0-0'], + ['^1.2.x+build', '>=1.2.0-0 <2.0.0-0'], + ['^1.x+meta-data', '>=1.0.0-0 <2.0.0-0'], + ['^1.x.x+build.123', '>=1.0.0-0 <2.0.0-0'], + ['~1.x+build', '>=1.0.0-0 <2.0.0-0'], + ['~1.x.x+build', '>=1.0.0-0 <2.0.0-0'], + ['~1.2.x+build', '>=1.2.0-0 <1.3.0-0'], + ['~1.x+meta-data', '>=1.0.0-0 <2.0.0-0'], + ['~1.x.x+build.123', '>=1.0.0-0 <2.0.0-0'], + ['^1.x.x+build || ~2.x.x+meta', '>=1.0.0-0 <2.0.0-0||>=2.0.0-0 <3.0.0-0'], + ['~1.x.x+build >2.x+meta', '>=1.0.0-0 <2.0.0-0 >=3.0.0-0'], + ['^1.x+build.123 <2.x.x+meta-data', '>=1.0.0-0 <2.0.0-0'], + ['1.x.x-alpha+build', '>=1.0.0-0 <2.0.0-0'], + ['>1.x.x-alpha+build', '>=2.0.0-0'], + ['>=1.x.x-alpha+build <2.x.x+build', '>=1.0.0-0 <2.0.0-0'], + ['1.x.x-alpha+build || 2.x.x+build', '>=1.0.0-0 <2.0.0-0||>=2.0.0-0 <3.0.0-0'], + ['v1.0+' + 'a'.repeat(249) + 'x6', '>=1.0.0-0 <1.1.0-0'], + ['1.2.3+' + 'a'.repeat(251) + ' - 2.0.0', '>=1.2.3-0 <2.0.1-0'], + ['1.x', '>=1.0.0-0 <2.0.0-0'], + ['1.X', '>=1.0.0-0 <2.0.0-0'], + ['1.*', '>=1.0.0-0 <2.0.0-0'], + ['1.x.x', '>=1.0.0-0 <2.0.0-0'], + ['1.2', '>=1.2.0-0 <1.3.0-0'], + ['1.2.X', '>=1.2.0-0 <1.3.0-0'], + ['=1', '>=1.0.0-0 <2.0.0-0'], + ['=1.x', '>=1.0.0-0 <2.0.0-0'], + ['=1.X', '>=1.0.0-0 <2.0.0-0'], + ['=1.*', '>=1.0.0-0 <2.0.0-0'], + ['=1.x.x', '>=1.0.0-0 <2.0.0-0'], + ['=1.2', '>=1.2.0-0 <1.3.0-0'], + ['=1.2.x', '>=1.2.0-0 <1.3.0-0'], + ['=1.2.X', '>=1.2.0-0 <1.3.0-0'], + ['=1.2.*', '>=1.2.0-0 <1.3.0-0'], + ['>1.x', '>=2.0.0-0'], + ['>1.X', '>=2.0.0-0'], + ['>1.*', '>=2.0.0-0'], + ['>1.x.x', '>=2.0.0-0'], + ['>1.2.x', '>=1.3.0-0'], + ['>1.2.X', '>=1.3.0-0'], + ['>1.2.*', '>=1.3.0-0'], + ['>=1.x', '>=1.0.0-0'], + ['>=1.X', '>=1.0.0-0'], + ['>=1.*', '>=1.0.0-0'], + ['>=1.x.x', '>=1.0.0-0'], + ['>=1.2', '>=1.2.0-0'], + ['>=1.2.x', '>=1.2.0-0'], + ['>=1.2.X', '>=1.2.0-0'], + ['>=1.2.*', '>=1.2.0-0'], + ['>=0.0.0', '>=0.0.0'], + ['>=0.0.0-0', '*'], + ['^0.0.0', '>=0.0.0 <0.0.1-0'], + ['* - 2.3.4', '<2.3.5-0'], + ['1.2.3 - *', '>=1.2.3-0'], + ['1.x - 2.3.x', '>=1.0.0-0 <2.4.0-0'], + ['1.2 - 3', '>=1.2.0-0 <4.0.0-0'], + ['1.2.3-beta.1 - 2.3.4', '>=1.2.3-beta.1 <2.3.5-0'], + ['1.2.3 - 2.3.4-beta.1', '>=1.2.3-0 <=2.3.4-beta.1'], + ['1.2\t-\n2.3', '>=1.2.0-0 <2.4.0-0'], + [`1.2.3 - 1.2.${MAX_SAFE_INTEGER}`, null], +]) + +// Inherit each option's changes, then override differences caused by combining them. +const combinedOverrides = new Map([ + ...looseOverrides, + ...prereleaseOverrides, + ['== 1', '>=1.0.0-0 <2.0.0-0'], + ['1 +a +b = 2', '>=1.0.0-0 <2.0.0-0 >=2.0.0-0 <3.0.0-0'], + ['1.2.3-dev== 1', '>=1.0.0-0 <2.0.0-0'], + ['1.x.5 || 2.x', '>=2.0.0-0 <3.0.0-0'], + ['>=09090', '>=9090.0.0-0'], + ['>=01.02 <02', '>=1.2.0-0 <2.0.0-0'], +]) + +const optionVariants = [ + [undefined, new Map()], + [{ loose: true }, looseOverrides], + [{ includePrerelease: true }, prereleaseOverrides], + [{ loose: true, includePrerelease: true }, combinedOverrides], ] + +module.exports = optionVariants.flatMap(([options, overrides]) => + ranges.map(([range, expected]) => [ + range, + overrides.has(range) ? overrides.get(range) : expected, + options, + ])) From 42a61fe1fb2d9487ecd8e2c52564bf70edb09d22 Mon Sep 17 00:00:00 2001 From: reggi Date: Tue, 13 Oct 2026 16:17:52 -0400 Subject: [PATCH 2/3] fix: keep range fixtures compatible with Node 10 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e40646ba-5ae5-433c-abed-2cdf7f08793f --- test/fixtures/range-parse.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/test/fixtures/range-parse.js b/test/fixtures/range-parse.js index 1a01ff90..4e3891da 100644 --- a/test/fixtures/range-parse.js +++ b/test/fixtures/range-parse.js @@ -546,9 +546,13 @@ const optionVariants = [ [{ loose: true, includePrerelease: true }, combinedOverrides], ] -module.exports = optionVariants.flatMap(([options, overrides]) => - ranges.map(([range, expected]) => [ - range, - overrides.has(range) ? overrides.get(range) : expected, - options, - ])) +module.exports = [] +for (const [options, overrides] of optionVariants) { + for (const [range, expected] of ranges) { + module.exports.push([ + range, + overrides.has(range) ? overrides.get(range) : expected, + options, + ]) + } +} From ea92bf59a81bee55e3fac6c4837ccbeec42fb6c5 Mon Sep 17 00:00:00 2001 From: reggi Date: Tue, 13 Oct 2026 16:28:06 -0400 Subject: [PATCH 3/3] chore: reduce range fixture test overhead Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e40646ba-5ae5-433c-abed-2cdf7f08793f --- test/classes/range.js | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/test/classes/range.js b/test/classes/range.js index 7161516d..311a99ac 100644 --- a/test/classes/range.js +++ b/test/classes/range.js @@ -18,20 +18,21 @@ test('range tests', t => { }) test('range parsing', t => { - t.plan(rangeParse.length) - rangeParse.forEach(([range, expect, options]) => - t.test(`${range} ${expect} ${JSON.stringify(options)}`, t => { - if (expect === null) { - t.throws(() => new Range(range, options), TypeError, `invalid range: ${range}`) - } else { - const parsed = new Range(range, options) - t.equal(parsed.range || '*', expect, `${range} => ${expect}`) - t.equal(parsed.range, - new Range(expect, { includePrerelease: parsed.includePrerelease }).range, - 'parsing both yields same result') - } - t.end() - })) + const assertions = rangeParse.reduce((count, [, expect]) => + count + (expect === null ? 1 : 2), 0) + t.plan(assertions) + rangeParse.forEach(([range, expect, options]) => { + const description = `${range} ${JSON.stringify(options)}` + if (expect === null) { + t.throws(() => new Range(range, options), TypeError, `invalid range: ${description}`) + } else { + const parsed = new Range(range, options) + t.equal(parsed.range || '*', expect, `${description} => ${expect}`) + t.equal(parsed.range, + new Range(expect, { includePrerelease: parsed.includePrerelease }).range, + `parsing both yields same result: ${description}`) + } + }) }) test('range parsing covers every option combination', t => {