mirror of
https://github.com/renovatebot/renovate.git
synced 2026-11-01 02:00:10 +00:00
3412de84ce
* refactor(lint): enable oxlint prefer-named-capture-group at error Numbered capture groups (m[1], $1) break silently when a regex is edited and group order shifts. Named groups (?<name>...) make intent explicit and accesses (m.groups.name) resilient to reordering. This enables the built-in eslint rule prefer-named-capture-group, surfacing 321 pre-existing unnamed-group violations across ~121 files to be burned down incrementally in follow-up commits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(versioning): name capture groups for prefer-named-capture-group Burns down all prefer-named-capture-group violations in lib/modules/versioning/. Unused capture groups (only reachable via .test() or whole-match) become non-capturing (?:...); groups whose value is read become named (?<name>...), with accesses rewritten to match.groups!.<name> (or destructured from .groups!) instead of positional match[n]/$n so a future edit that reorders groups cannot silently break the reader. Two cases needed extra care to preserve behavior: - semver/common.ts and poetry/patterns.ts split() on a regex whose capture group is relied upon by String.prototype.split to keep the separator in the output array; the group is kept capturing and named, not converted to non-capturing. - hashicorp/convertor.ts's semverRegex is only ever embedded via .source into a named `(?<version>...)` group elsewhere and never matched directly, so its internal groups are safe to make non-capturing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(datasource): name capture groups for prefer-named-capture-group Burns down all prefer-named-capture-group violations under lib/modules/datasource/. Same treatment as the versioning module: unused groups become non-capturing, used groups get names, and positional match[n] reads move to match.groups!.<name>. docker/ecr.ts and pypi/common.ts keep their positional destructuring (`const [, region] = ...`, `!!m &&`) working unchanged since a named capture group is still addressable by its numeric index in JavaScript — only pypi/common.ts's access was rewritten to m.groups!.owner for clarity since it read a single group directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(platform): name capture groups for prefer-named-capture-group Burns down all prefer-named-capture-group violations under lib/modules/platform/. Uses the repo's established idiom of `match.groups?.<name>` after an optional-chained exec()/match() (see bitbucket/index.ts hostnameWithoutApiPrefix, gerrit/utils.ts extractSourceBranch) and `$<name>` in String.replace replacement strings. Unused groups (markdown/HTML massaging regexes matched only to be stripped) become non-capturing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(manager): name capture groups in ansible-galaxy for prefer-named-capture-group Burns down prefer-named-capture-group violations in lib/modules/manager/ansible-galaxy/. newBlockRegEx and blockLineRegEx dropped their unused outer wrapping group (only the inner key/value groups were ever read) and named the key/value groups; nameMatchRegex's remaining unused groups (protocol/user-info/port alternatives) become non-capturing since only source/hostname/depName/version are read via .groups. Accesses in collections.ts and roles.ts move from lineMatch[2]/lineMatch[3] to lineMatch.groups!.key/value. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(manager): name capture groups for prefer-named-capture-group (batch 1) Burns down prefer-named-capture-group violations across the first alphabetical slice of lib/modules/manager/ (ansible through mix). Used groups get names with accesses rewritten to match.groups!.<name> (or $<name> in replacement strings); groups whose value is never read become non-capturing (?:...). Two String.split() call sites (git-submodules/extract.ts, meteor/extract.ts) keep the established "named group still readable positionally from the split() result array" idiom instead of restructuring the destructuring. bazel/rules/go.ts and buildpacks/extract.ts dockerRef verified byte-for-byte equivalent before/after via a quick node script since several nested groups collapsed to non-capturing at once. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(manager): name capture groups for prefer-named-capture-group (batch 2) Burns down prefer-named-capture-group violations across the remaining alphabetical slice of lib/modules/manager/ (npm through terragrunt). Same treatment as batch 1: used groups get names with accesses moved to match.groups!.<name> or $<name> replacement strings; unused groups (wrapping alternations, quantifier repetition groups only consumed via .test(), or values never read from a match) become non-capturing. terraform/extractors/others/modules.ts and terragrunt/modules.ts share near-identical URL-parsing regexes (github/bitbucket/git/azure-devops ref matchers) — the same set of always-unused `depth=`/`&depth=` query-string groups is converted to non-capturing in both files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(config): name capture groups for prefer-named-capture-group Burns down prefer-named-capture-group violations in lib/config/. Used groups get names with replacement strings switched from $1/$2 to $<name>; the unused separator group in schedule-migration.ts's afterBeforeRegex becomes non-capturing. presets/parse.ts keeps assigning through the already-declared `repo` variable rather than array-destructuring the exec() result, matching this file's existing style for the other named-group regexes just above it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(util): name capture groups for prefer-named-capture-group Burns down prefer-named-capture-group violations across lib/util/ and lib/logger/. Same treatment as previous batches: named groups for values that are read (via $<name> replacement strings, .groups!.<name>, or destructured from .groups), non-capturing groups where nothing reads the captured value (e.g. semantic.ts's angular-commit .test()-only regex). www-authenticate.ts's tokenizer entries each get their own named group but keep accessing the match generically via match[1], since a positionally-indexed named group is still readable by index. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(workers): name capture groups for prefer-named-capture-group Burns down prefer-named-capture-group violations in lib/workers/. Used groups get names with $<name> replacement strings; unused wrapper/alternation groups (e.g. semantic-commit-message.ts's optional scope/issue wrappers, release-notes.ts's date-heading and horizontal-rule matchers that are only probed with .search()/replaced wholesale) become non-capturing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(test,tools): name capture groups for prefer-named-capture-group Finishes the prefer-named-capture-group burndown: test/, tools/, and vitest.config.mts. Groups whose value is read get names ($<name> replacement strings or .groups.<name> access); groups only used via .test()/.search() or that feed a .match(/…/g) call (whose return value is the array of whole matches, not captured groups) become non-capturing. This is the last remaining slice of pre-existing violations, so the rule (already enabled at error repo-wide) now passes with zero findings across the whole codebase. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * fix prettier failure * refactor(lint): use a named capture group in `v8-ignore-no-count` New rule from main trips the newly enabled `prefer-named-capture-group` oxlint rule. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
413 lines
13 KiB
TypeScript
413 lines
13 KiB
TypeScript
import { isNumber } from '@sindresorhus/is';
|
|
import semver from 'semver';
|
|
import { parseRange } from 'semver-utils';
|
|
import { logger } from '../../../logger/index.ts';
|
|
import type { RangeStrategy } from '../../../types/versioning.ts';
|
|
import { regEx } from '../../../util/regex.ts';
|
|
import { api as npm } from '../npm/index.ts';
|
|
import type { NewValueConfig, VersioningApi } from '../types.ts';
|
|
|
|
export const id = 'composer';
|
|
export const displayName = 'Composer';
|
|
export const urls = [
|
|
'[Composer versions](https://getcomposer.org/doc/articles/versions.md)',
|
|
'[composer/semver package](https://packagist.org/packages/composer/semver)',
|
|
'[Tilde and caret constraints](https://madewithlove.be/tilde-and-caret-constraints/)',
|
|
'[Composer semver checker](https://semver.mwl.be)',
|
|
];
|
|
export const supportsRanges = true;
|
|
export const supportedRangeStrategies: RangeStrategy[] = [
|
|
'bump',
|
|
'widen',
|
|
'replace',
|
|
'update-lockfile',
|
|
];
|
|
|
|
function getVersionParts(input: string): [string, string] {
|
|
const versionParts = input.split('-');
|
|
if (versionParts.length === 1) {
|
|
return [input, ''];
|
|
}
|
|
|
|
return [versionParts[0], `-${versionParts[1]}`];
|
|
}
|
|
|
|
function padZeroes(input: string): string {
|
|
const [output, stability] = getVersionParts(input);
|
|
|
|
const sections = output.split('.');
|
|
while (sections.length < 3) {
|
|
sections.push('0');
|
|
}
|
|
return `${sections.join('.')}${stability}`;
|
|
}
|
|
|
|
function convertStabilityModifier(input: string): string {
|
|
// Handle stability modifiers.
|
|
const versionParts = input.split('@');
|
|
if (versionParts.length === 1) {
|
|
return input;
|
|
}
|
|
|
|
// 1.0@beta2 to 1.0-beta.2
|
|
const stability = versionParts[1].replace(
|
|
regEx(/(?:^|\s)(?<label>beta|alpha|rc)(?<num>[1-9][0-9]*)(?: |$)/gi),
|
|
'$<label>.$<num>',
|
|
);
|
|
|
|
// If there is a stability part, npm semver expects the version
|
|
// to be full
|
|
return `${padZeroes(versionParts[0])}-${stability}`;
|
|
}
|
|
|
|
function normalizeVersion(input: string): string {
|
|
let output = input;
|
|
// Strip leading + prefix (not a valid composer constraint operator)
|
|
output = output.replace(regEx(/^\+/), '');
|
|
output = output.replace(regEx(/(?<prefix>^|>|>=|\^|~)v/i), '$<prefix>');
|
|
return convertStabilityModifier(output);
|
|
}
|
|
|
|
/**
|
|
* @param versions Version list in any format, it recognizes the specific patch format x.x.x-pXX
|
|
* @param range Range in composer format
|
|
* @param minMode If true, it will calculate minSatisfyingVersion, if false, it calculates the maxSatisfyingVersion
|
|
* @returns min or max satisfyingVersion from the input
|
|
*/
|
|
function calculateSatisfyingVersionIntenal(
|
|
versions: string[],
|
|
range: string,
|
|
minMode: boolean,
|
|
): string | null {
|
|
// Because composer -p versions are considered stable, we have to remove the suffix for the npm.XXX functions.
|
|
const versionsMapped = versions.map((x) => {
|
|
return {
|
|
origianl: x,
|
|
cleaned: removeComposerSpecificPatchPart(x),
|
|
npmVariant: composer2npm(removeComposerSpecificPatchPart(x)[0]),
|
|
};
|
|
});
|
|
|
|
const npmVersions = versionsMapped.map((x) => x.npmVariant);
|
|
const npmVersion = minMode
|
|
? npm.minSatisfyingVersion(npmVersions, composer2npm(range))
|
|
: npm.getSatisfyingVersion(npmVersions, composer2npm(range));
|
|
|
|
if (!npmVersion) {
|
|
return null;
|
|
}
|
|
|
|
// After we find the npm versions, we select from them back in the mapping the possible patches.
|
|
const candidates = versionsMapped
|
|
.filter((x) => x.npmVariant === npmVersion)
|
|
.sort((a, b) => (minMode ? 1 : -1) * sortVersions(a.origianl, b.origianl));
|
|
|
|
return candidates[0].origianl;
|
|
}
|
|
|
|
/**
|
|
* @param intput Version in any format, it recognizes the specific patch format x.x.x-pXX
|
|
* @returns If input contains the specific patch, it returns the input with removed the patch and true, otherwise it retunrs the same string and false.
|
|
*/
|
|
function removeComposerSpecificPatchPart(input: string): [string, boolean] {
|
|
// the regex is based on the original from composer implementation https://github.com/composer/semver/blob/fa1ec24f0ab1efe642671ec15c51a3ab879f59bf/src/VersionParser.php#L137
|
|
const pattern = regEx(
|
|
/^v?\d+(?:\.\d+(?:\.\d+(?:\.\d+)?)?)?(?<suffix>-p[1-9]\d*)$/gi,
|
|
);
|
|
const match = pattern.exec(input);
|
|
|
|
return match
|
|
? [input.replace(match.groups!.suffix, ''), true]
|
|
: [input, false];
|
|
}
|
|
|
|
function composer2npm(input: string): string {
|
|
return input
|
|
.split(regEx(/\s*\|\|?\s*/g))
|
|
.map((part): string => {
|
|
const cleanInput = normalizeVersion(part);
|
|
if (npm.isVersion(cleanInput)) {
|
|
return cleanInput;
|
|
}
|
|
if (npm.isVersion(padZeroes(cleanInput))) {
|
|
return padZeroes(cleanInput);
|
|
}
|
|
const [versionId, stability] = getVersionParts(cleanInput);
|
|
let output = versionId;
|
|
|
|
// ~4 to ^4 and ~4.1 to ^4.1
|
|
output = output.replace(
|
|
regEx(/(?:^|\s)~(?<minVersion>[1-9][0-9]*(?:\.[0-9]*)?)(?: |$)/g),
|
|
'^$<minVersion>',
|
|
);
|
|
// ~0.4 to >=0.4 <1
|
|
output = output.replace(
|
|
regEx(/(?:^|\s)~(?<minVersion>0\.[1-9][0-9]*)(?: |$)/g),
|
|
'>=$<minVersion> <1',
|
|
);
|
|
|
|
// add extra digits to <8-DEV and <8.0-DEV
|
|
output = output
|
|
.replace(regEx(/^(?<version><\d+(?:\.\d+)?)$/g), '$<version>.0')
|
|
.replace(regEx(/^(?<version><\d+(?:\.\d+)?)$/g), '$<version>.0');
|
|
|
|
return output + stability;
|
|
})
|
|
.map((part) =>
|
|
part.replace(
|
|
regEx(/(?<letter>[a-z])(?<digit>[0-9])/gi),
|
|
'$<letter>.$<digit>',
|
|
),
|
|
)
|
|
.join(' || ');
|
|
}
|
|
|
|
function equals(a: string, b: string): boolean {
|
|
return npm.equals(composer2npm(a), composer2npm(b));
|
|
}
|
|
|
|
function getMajor(version: string): number | null {
|
|
const semverVersion = semver.coerce(composer2npm(version));
|
|
return semverVersion ? npm.getMajor(semverVersion) : null;
|
|
}
|
|
|
|
function getMinor(version: string): number | null {
|
|
const semverVersion = semver.coerce(composer2npm(version));
|
|
return semverVersion ? npm.getMinor(semverVersion) : null;
|
|
}
|
|
|
|
function getPatch(version: string): number | null {
|
|
const semverVersion = semver.coerce(composer2npm(version));
|
|
|
|
// This returns only the numbers without the optional `-pXX` patch version supported by composer. Fixing that would require a bigger
|
|
// refactoring, because the API supports only numbers.
|
|
return semverVersion ? npm.getPatch(semverVersion) : null;
|
|
}
|
|
|
|
function isGreaterThan(a: string, b: string): boolean {
|
|
return sortVersions(a, b) === 1;
|
|
}
|
|
|
|
function isLessThanRange(version: string, range: string): boolean {
|
|
return !!npm.isLessThanRange?.(composer2npm(version), composer2npm(range));
|
|
}
|
|
|
|
function isSingleVersion(input: string): boolean {
|
|
return !!input && npm.isSingleVersion(composer2npm(input));
|
|
}
|
|
|
|
function isStable(version: string): boolean {
|
|
if (version) {
|
|
// Composer considers patches `-pXX` as stable: https://github.com/composer/semver/blob/fa1ec24f0ab1efe642671ec15c51a3ab879f59bf/src/VersionParser.php#L568 but npm not.
|
|
// In order to be able to use the standard npm.isStable function, we remove the potential patch version for the check.
|
|
const [withoutPatch] = removeComposerSpecificPatchPart(version);
|
|
return npm.isStable(composer2npm(withoutPatch));
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export function isValid(input: string): boolean {
|
|
return !!input && npm.isValid(composer2npm(input));
|
|
}
|
|
|
|
export function isVersion(input: string): boolean {
|
|
return !!input && npm.isVersion(composer2npm(input));
|
|
}
|
|
|
|
function matches(version: string, range: string): boolean {
|
|
return npm.matches(composer2npm(version), composer2npm(range));
|
|
}
|
|
|
|
function getSatisfyingVersion(
|
|
versions: string[],
|
|
range: string,
|
|
): string | null {
|
|
return calculateSatisfyingVersionIntenal(versions, range, false);
|
|
}
|
|
|
|
function minSatisfyingVersion(
|
|
versions: string[],
|
|
range: string,
|
|
): string | null {
|
|
return calculateSatisfyingVersionIntenal(versions, range, true);
|
|
}
|
|
|
|
function subset(subRange: string, superRange: string): boolean | undefined {
|
|
try {
|
|
return npm.subset!(composer2npm(subRange), composer2npm(superRange));
|
|
} catch (err) {
|
|
logger.trace({ err }, 'composer.subset error');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function intersects(subRange: string, superRange: string): boolean {
|
|
try {
|
|
return npm.intersects!(composer2npm(subRange), composer2npm(superRange));
|
|
} catch (err) {
|
|
logger.trace({ err }, 'composer.intersects error');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function getNewValue({
|
|
currentValue,
|
|
rangeStrategy,
|
|
currentVersion,
|
|
newVersion,
|
|
}: NewValueConfig): string | null {
|
|
if (rangeStrategy === 'update-lockfile') {
|
|
if (matches(newVersion, currentValue)) {
|
|
return currentValue;
|
|
}
|
|
return getNewValue({
|
|
currentValue,
|
|
rangeStrategy: 'replace',
|
|
currentVersion,
|
|
newVersion,
|
|
});
|
|
}
|
|
const currentMajor = currentVersion ? getMajor(currentVersion) : null;
|
|
const toMajor = getMajor(newVersion);
|
|
const toMinor = getMinor(newVersion);
|
|
let newValue: string | null = null;
|
|
if (isVersion(currentValue)) {
|
|
newValue = newVersion;
|
|
} else if (regEx(/^[~^](?:0\.[1-9][0-9]*)$/).test(currentValue)) {
|
|
const operator = currentValue.substring(0, 1);
|
|
// handle ~0.4 case first
|
|
if (toMajor === 0) {
|
|
// TODO: types (#22198)
|
|
newValue = `${operator}0.${toMinor!}`;
|
|
} else {
|
|
// TODO: types (#22198)
|
|
newValue = `${operator}${toMajor!}.0`;
|
|
}
|
|
} else if (regEx(/^[~^](?:[0-9]*)$/).test(currentValue)) {
|
|
// handle ~4 case
|
|
const operator = currentValue.substring(0, 1);
|
|
// TODO: types (#22198)
|
|
newValue = `${operator}${toMajor!}`;
|
|
} else if (
|
|
toMajor &&
|
|
regEx(/^[~^](?:[0-9]*(?:\.[0-9]*)?)$/).test(currentValue)
|
|
) {
|
|
const operator = currentValue.substring(0, 1);
|
|
if (rangeStrategy === 'bump') {
|
|
newValue = `${operator}${newVersion}`;
|
|
} else if ((isNumber(currentMajor) && toMajor > currentMajor) || !toMinor) {
|
|
// handle ~4.1 case
|
|
newValue = `${operator}${toMajor}.0`;
|
|
} else {
|
|
newValue = `${operator}${toMajor}.${toMinor}`;
|
|
}
|
|
} else if (
|
|
currentVersion &&
|
|
npm.isVersion(padZeroes(normalizeVersion(newVersion))) &&
|
|
npm.isValid(normalizeVersion(currentValue)) &&
|
|
composer2npm(currentValue) === normalizeVersion(currentValue)
|
|
) {
|
|
newValue = npm.getNewValue({
|
|
currentValue: normalizeVersion(currentValue),
|
|
rangeStrategy,
|
|
currentVersion: padZeroes(normalizeVersion(currentVersion)),
|
|
newVersion: padZeroes(normalizeVersion(newVersion)),
|
|
});
|
|
}
|
|
|
|
if (rangeStrategy === 'widen' && matches(newVersion, currentValue)) {
|
|
newValue = currentValue;
|
|
} else {
|
|
const hasOr = currentValue.includes(' || ');
|
|
if (hasOr || rangeStrategy === 'widen') {
|
|
const splitValues = currentValue.split('||');
|
|
const lastValue = splitValues.at(-1)!;
|
|
const replacementValue = getNewValue({
|
|
currentValue: lastValue.trim(),
|
|
rangeStrategy: 'replace',
|
|
currentVersion,
|
|
newVersion,
|
|
});
|
|
if (rangeStrategy === 'replace') {
|
|
newValue = replacementValue;
|
|
} else if (replacementValue) {
|
|
const parsedRange = parseRange(replacementValue);
|
|
const element = parsedRange.at(-1)!;
|
|
if (element.operator?.startsWith('<')) {
|
|
const splitCurrent = currentValue.split(element.operator);
|
|
splitCurrent.pop();
|
|
newValue = `${splitCurrent.join(element.operator)}${replacementValue}`;
|
|
} else {
|
|
newValue = `${currentValue} || ${replacementValue}`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!newValue) {
|
|
logger.warn(
|
|
{ currentValue, rangeStrategy, currentVersion, newVersion },
|
|
'Unsupported composer value',
|
|
);
|
|
newValue = newVersion;
|
|
}
|
|
if (currentValue.split('.')[0].includes('v')) {
|
|
newValue = newValue.replace(regEx(/(?<digit>[0-9])/), 'v$<digit>');
|
|
}
|
|
|
|
// Preserve original min-stability specifier
|
|
if (currentValue.includes('@')) {
|
|
newValue += `@${currentValue.split('@')[1]}`;
|
|
}
|
|
|
|
return newValue;
|
|
}
|
|
|
|
function sortVersions(a: string, b: string): number {
|
|
const [aWithoutPatch, aContainsPatch] = removeComposerSpecificPatchPart(a);
|
|
const [bWithoutPatch, bContainsPatch] = removeComposerSpecificPatchPart(b);
|
|
|
|
if (aContainsPatch === bContainsPatch) {
|
|
// If both [a and b] contain patch version or both [a and b] do not contain patch version, then npm comparison deliveres correct results
|
|
return npm.sortVersions(composer2npm(a), composer2npm(b));
|
|
}
|
|
if (npm.equals(composer2npm(aWithoutPatch), composer2npm(bWithoutPatch))) {
|
|
// If only one [a or b] contains patch version and the parts without patch versions are equal, then the version with patch is greater (this is the case where npm comparison fails)
|
|
return aContainsPatch ? 1 : -1;
|
|
}
|
|
// All other cases can be compared correctly by npm
|
|
return npm.sortVersions(composer2npm(a), composer2npm(b));
|
|
}
|
|
|
|
function isCompatible(version: string): boolean {
|
|
return isVersion(version);
|
|
}
|
|
|
|
function isBreaking(current: string, version: string): boolean {
|
|
return npm.isBreaking!(composer2npm(current), composer2npm(version));
|
|
}
|
|
|
|
export const api: VersioningApi = {
|
|
equals,
|
|
getMajor,
|
|
getMinor,
|
|
getPatch,
|
|
isBreaking,
|
|
isCompatible,
|
|
isGreaterThan,
|
|
isLessThanRange,
|
|
isSingleVersion,
|
|
isStable,
|
|
isValid,
|
|
isVersion,
|
|
matches,
|
|
getSatisfyingVersion,
|
|
minSatisfyingVersion,
|
|
getNewValue,
|
|
sortVersions,
|
|
subset,
|
|
intersects,
|
|
};
|
|
export default api;
|