// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. import { ALL } from "./constants.ts"; import type { SemVerRange } from "./types.ts"; import { CARET, HYPHENRANGE, re, STAR, TILDE, XRANGE } from "./_shared.ts"; import { parseComparator } from "./parse_comparator.ts"; // ~, ~> --> * (any, kinda silly) // ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0 <3.0.0 // ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0 <2.1.0 // ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0 <1.3.0 // ~1.2.3, ~>1.2.3 --> >=1.2.3 <1.3.0 // ~1.2.0, ~>1.2.0 --> >=1.2.0 <1.3.0 function replaceTildes(comp: string): string { return comp .trim() .split(/\s+/) .map((comp) => replaceTilde(comp)) .join(" "); } function replaceTilde(comp: string): string { const r: RegExp = re[TILDE]; return comp.replace( r, (_: string, M: string, m: string, p: string, pr: string) => { let ret: string; if (isX(M)) { ret = ""; } else if (isX(m)) { ret = ">=" + M + ".0.0 <" + (+M + 1) + ".0.0"; } else if (isX(p)) { // ~1.2 == >=1.2.0 <1.3.0 ret = ">=" + M + "." + m + ".0 <" + M + "." + (+m + 1) + ".0"; } else if (pr) { ret = ">=" + M + "." + m + "." + p + "-" + pr + " <" + M + "." + (+m + 1) + ".0"; } else { // ~1.2.3 == >=1.2.3 <1.3.0 ret = ">=" + M + "." + m + "." + p + " <" + M + "." + (+m + 1) + ".0"; } return ret; }, ); } // ^ --> * (any, kinda silly) // ^2, ^2.x, ^2.x.x --> >=2.0.0 <3.0.0 // ^2.0, ^2.0.x --> >=2.0.0 <3.0.0 // ^1.2, ^1.2.x --> >=1.2.0 <2.0.0 // ^1.2.3 --> >=1.2.3 <2.0.0 // ^1.2.0 --> >=1.2.0 <2.0.0 function replaceCarets(comp: string): string { return comp .trim() .split(/\s+/) .map((comp) => replaceCaret(comp)) .join(" "); } function replaceCaret(comp: string): string { const r: RegExp = re[CARET]; return comp.replace(r, (_: string, M, m, p, pr) => { let ret: string; if (isX(M)) { ret = ""; } else if (isX(m)) { ret = ">=" + M + ".0.0 <" + (+M + 1) + ".0.0"; } else if (isX(p)) { if (M === "0") { ret = ">=" + M + "." + m + ".0 <" + M + "." + (+m + 1) + ".0"; } else { ret = ">=" + M + "." + m + ".0 <" + (+M + 1) + ".0.0"; } } else if (pr) { if (M === "0") { if (m === "0") { ret = ">=" + M + "." + m + "." + p + "-" + pr + " <" + M + "." + m + "." + (+p + 1); } else { ret = ">=" + M + "." + m + "." + p + "-" + pr + " <" + M + "." + (+m + 1) + ".0"; } } else { ret = ">=" + M + "." + m + "." + p + "-" + pr + " <" + (+M + 1) + ".0.0"; } } else { if (M === "0") { if (m === "0") { ret = ">=" + M + "." + m + "." + p + " <" + M + "." + m + "." + (+p + 1); } else { ret = ">=" + M + "." + m + "." + p + " <" + M + "." + (+m + 1) + ".0"; } } else { ret = ">=" + M + "." + m + "." + p + " <" + (+M + 1) + ".0.0"; } } return ret; }); } function replaceXRanges(comp: string): string { return comp .split(/\s+/) .map((comp) => replaceXRange(comp)) .join(" "); } function replaceXRange(comp: string): string { comp = comp.trim(); const r: RegExp = re[XRANGE]; return comp.replace(r, (ret: string, gtlt, M, m, p, _pr) => { const xM: boolean = isX(M); const xm: boolean = xM || isX(m); const xp: boolean = xm || isX(p); const anyX: boolean = xp; if (gtlt === "=" && anyX) { gtlt = ""; } if (xM) { if (gtlt === ">" || gtlt === "<") { // nothing is allowed ret = "<0.0.0"; } else { // nothing is forbidden ret = "*"; } } else if (gtlt && anyX) { // we know patch is an x, because we have any x at all. // replace X with 0 if (xm) { m = 0; } p = 0; if (gtlt === ">") { // >1 => >=2.0.0 // >1.2 => >=1.3.0 // >1.2.3 => >= 1.2.4 gtlt = ">="; if (xm) { M = +M + 1; m = 0; p = 0; } else { m = +m + 1; p = 0; } } else if (gtlt === "<=") { // <=0.7.x is actually <0.8.0, since any 0.7.x should // pass. Similarly, <=7.x is actually <8.0.0, etc. gtlt = "<"; if (xm) { M = +M + 1; } else { m = +m + 1; } } ret = gtlt + M + "." + m + "." + p; } else if (xm) { ret = ">=" + M + ".0.0 <" + (+M + 1) + ".0.0"; } else if (xp) { ret = ">=" + M + "." + m + ".0 <" + M + "." + (+m + 1) + ".0"; } return ret; }); } // Because * is AND-ed with everything else in the comparator, // and '' means "any version", just remove the *s entirely. function replaceStars(comp: string): string { return comp.trim().replace(re[STAR], ""); } // This function is passed to string.replace(re[HYPHENRANGE]) // M, m, patch, prerelease, build // 1.2 - 3.4.5 -> >=1.2.0 <=3.4.5 // 1.2.3 - 3.4 -> >=1.2.0 <3.5.0 Any 3.4.x will do // 1.2 - 3.4 -> >=1.2.0 <3.5.0 function hyphenReplace( _$0: string, from: string, fM: string, fm: string, fp: string, _fpr: string, _fb: string, to: string, tM: string, tm: string, tp: string, tpr: string, _tb: string, ) { if (isX(fM)) { from = ""; } else if (isX(fm)) { from = ">=" + fM + ".0.0"; } else if (isX(fp)) { from = ">=" + fM + "." + fm + ".0"; } else { from = ">=" + from; } if (isX(tM)) { to = ""; } else if (isX(tm)) { to = "<" + (+tM + 1) + ".0.0"; } else if (isX(tp)) { to = "<" + tM + "." + (+tm + 1) + ".0"; } else if (tpr) { to = "<=" + tM + "." + tm + "." + tp + "-" + tpr; } else { to = "<=" + to; } return (from + " " + to).trim(); } function isX(id: string): boolean { return !id || id.toLowerCase() === "x" || id === "*"; } /** * Parses a range string into a SemVerRange object or throws a TypeError. * @param range The range string * @returns A valid semantic version range */ export function parseRange(range: string): SemVerRange { // handle spaces around and between comparator and version range = range.trim().replaceAll(/(?<=<|>|=) /g, ""); if (range === "") { return { ranges: [[ALL]] }; } // Split into groups of comparators, these are considered OR'd together. const ranges = range .trim() .split(/\s*\|\|\s*/) .map((range) => { // convert `1.2.3 - 1.2.4` into `>=1.2.3 <=1.2.4` const hr: RegExp = re[HYPHENRANGE]; range = range.replace(hr, hyphenReplace); range = replaceCarets(range); range = replaceTildes(range); range = replaceXRanges(range); range = replaceStars(range); // At this point, the range is completely trimmed and // ready to be split into comparators. // These are considered AND's if (range === "") { return [ALL]; } else { return range .split(" ") .filter((r) => r) .map((r) => parseComparator(r)); } }); return { ranges }; }