import { isNonEmptyArray, isNonEmptyStringAndNotWhitespace, } from '@sindresorhus/is'; import { logger } from '../../logger/index.ts'; import { type ConstraintName, isAdditionalConstraintName, } from '../../util/exec/types.ts'; import { filterMap } from '../../util/filter-map.ts'; import { regEx } from '../../util/regex.ts'; import * as allVersioning from '../versioning/index.ts'; import { defaultVersioning } from '../versioning/index.ts'; import datasources from './api.ts'; import { CustomDatasource } from './custom/index.ts'; import type { DatasourceApi, GetPkgReleasesConfig, ReleaseResult, } from './types.ts'; export function getDatasourceFor(datasource: string): DatasourceApi | null { if (datasource?.startsWith('custom.')) { return getDatasourceFor(CustomDatasource.id); } return datasources.get(datasource) ?? null; } /** * The host types of the datasources whose requests use another host type than their id. */ const datasourceHostTypes: Record = { 'sbt-package': 'sbt', 'sbt-plugin': 'sbt', }; /** * Returns the host type the requests of `datasource` use, such as `custom` for `custom.`, or `null` when there is no such datasource. */ export function getDatasourceHostType(datasource: string): string | null { const id = getDatasourceFor(datasource)?.id; if (!id) { return null; } return datasourceHostTypes[id] ?? id; } export function getDefaultVersioning( datasourceName: string | undefined, ): string { if (!datasourceName) { return defaultVersioning.id; } const datasource = getDatasourceFor(datasourceName); if (!datasource) { logger.warn({ datasourceName }, 'Missing datasource!'); return defaultVersioning.id; } if (!datasource.defaultVersioning) { return defaultVersioning.id; } return datasource.defaultVersioning; } export function isGetPkgReleasesConfig( input: unknown, ): input is GetPkgReleasesConfig { return ( isNonEmptyStringAndNotWhitespace( (input as GetPkgReleasesConfig).datasource, ) && isNonEmptyStringAndNotWhitespace( (input as GetPkgReleasesConfig).packageName, ) ); } export function applyVersionCompatibility( releaseResult: ReleaseResult, versionCompatibility: string | undefined, currentCompatibility: string | undefined, ): ReleaseResult { if (!versionCompatibility) { return releaseResult; } const versionCompatibilityRegEx = regEx(versionCompatibility); releaseResult.releases = filterMap(releaseResult.releases, (release) => { const regexResult = versionCompatibilityRegEx.exec(release.version); if (!regexResult?.groups?.version) { logger.trace( { releaseVersion: release.version, versionCompatibility }, 'versionCompatibility: Does not match regex', ); return null; } if (regexResult?.groups?.compatibility !== currentCompatibility) { logger.trace( { releaseVersion: release.version, versionCompatibility }, 'versionCompatibility: Does not match compatibility', ); return null; } logger.trace( { releaseVersion: release.version, versionCompatibility, version: regexResult.groups.version, compatibility: regexResult.groups.compatibility, }, 'versionCompatibility: matches', ); release.versionOrig ??= release.version; release.version = regexResult.groups.version; return release; }); return releaseResult; } export function applyExtractVersion( releaseResult: ReleaseResult, extractVersion: string | undefined, ): ReleaseResult { if (!extractVersion) { return releaseResult; } const extractVersionRegEx = regEx(extractVersion); releaseResult.releases = filterMap(releaseResult.releases, (release) => { const version = extractVersionRegEx.exec(release.version)?.groups?.version; if (!version) { return null; } release.versionOrig = release.version; release.version = version; return release; }); return releaseResult; } export function filterValidVersions< Config extends Pick, >(releaseResult: ReleaseResult, config: Config): ReleaseResult { const versioningName = config.versioning ?? getDefaultVersioning(config.datasource); const versioning = allVersioning.get(versioningName); releaseResult.releases = filterMap(releaseResult.releases, (release) => versioning.isVersion(release.version) ? release : null, ); return releaseResult; } export function sortAndRemoveDuplicates< Config extends Pick, >(releaseResult: ReleaseResult, config: Config): ReleaseResult { const versioningName = config.versioning ?? getDefaultVersioning(config.datasource); const versioning = allVersioning.get(versioningName); releaseResult.releases = releaseResult.releases.sort((a, b) => versioning.sortVersions(a.version, b.version), ); // Once releases are sorted, deduplication is straightforward and efficient let previousVersion: string | null = null; releaseResult.releases = filterMap(releaseResult.releases, (release) => { if (previousVersion === release.version) { return null; } previousVersion = release.version; return release; }); return releaseResult; } export function applyConstraintsFiltering< Config extends Pick< GetPkgReleasesConfig, | 'constraintsFiltering' | 'constraintsVersioning' | 'versioning' | 'datasource' | 'constraints' | 'packageName' >, >(releaseResult: ReleaseResult, config: Config): ReleaseResult { if (config?.constraintsFiltering !== 'strict') { for (const release of releaseResult.releases) { delete release.constraints; } return releaseResult; } const versioningName = config.versioning ?? getDefaultVersioning(config.datasource); const versioning = allVersioning.get(versioningName); const configConstraints = config.constraints; const filteredReleases: string[] = []; const startingLength = releaseResult.releases.length; releaseResult.releases = filterMap(releaseResult.releases, (release) => { logger.trace( { release, versioning: versioningName, }, `applyConstraintsFiltering(${release.version})`, ); const releaseConstraints = release.constraints; delete release.constraints; logger.trace( { release, versioning: versioningName, configConstraints: configConstraints ?? 'undefined', releaseConstraints: releaseConstraints ?? 'undefined', }, `applyConstraintsFiltering(${release.version}): checking constraints`, ); if (!configConstraints || !releaseConstraints) { return release; } for (const [name, configConstraint] of Object.entries( configConstraints, ) as [ConstraintName, string][]) { let constraintVersioningName = versioningName; if ( isAdditionalConstraintName(name) && config.constraintsVersioning?.[name] ) { const val = config.constraintsVersioning[name]; logger.once.debug( { packageName: config.packageName, release }, `applyConstraintsFiltering(${config.packageName}, ${name}): overriding constraintsVersioning from ${constraintVersioningName} to ${val}`, ); logger.trace( { packageName: config.packageName, release }, `applyConstraintsFiltering(${release.version}): overriding constraintsVersioning from ${constraintVersioningName} to ${val}`, ); constraintVersioningName = val; } const constraintVersioning = allVersioning.get(constraintVersioningName); logger.trace( { release, versioning: constraintVersioningName, constraintName: name, constraint: configConstraint, }, `applyConstraintsFiltering(${release.version}) for constraint ${name}`, ); const isValid = constraintVersioning.isValid(configConstraint); logger.trace( { release, versioning: constraintVersioningName, }, `applyConstraintsFiltering(${release.version}): versioning.isValid(${configConstraint})=${isValid}`, ); if (!isValid) { logger.once.warn( { packageName: config.packageName, constraint: configConstraint, versioning: constraintVersioningName, }, 'Invalid constraint used with strict constraintsFiltering', ); continue; } const constraint = releaseConstraints[name]; logger.trace( { release, versioning: constraintVersioningName, }, `applyConstraintsFiltering(${release.version}): releaseConstraints[${name}]=${JSON.stringify(constraint)}`, ); if (!isNonEmptyArray(constraint)) { // A release with no constraints is OK continue; } let satisfiesConstraints = false; for (const releaseConstraint of constraint) { logger.trace( { release, versioning: constraintVersioningName, releaseConstraint, }, `applyConstraintsFiltering(${release.version}): releaseConstraint=${releaseConstraint}`, ); if (!releaseConstraint) { satisfiesConstraints = true; logger.once.debug( { packageName: config.packageName, versioning: constraintVersioningName, constraint: releaseConstraint, }, 'Undefined release constraint', ); break; } const isValidConstraintVersion = constraintVersioning.isValid(releaseConstraint); logger.trace( { release, versioning: constraintVersioningName, releaseConstraint, }, `applyConstraintsFiltering(${release.version}): constraintVersioning.isValid(${releaseConstraint})=${isValidConstraintVersion}`, ); const isValidVersion = versioning.isValid(releaseConstraint); logger.trace( { release, versioning: constraintVersioningName, releaseConstraint, }, `applyConstraintsFiltering(${release.version}): versioning.isValid(${releaseConstraint})=${isValidVersion}`, ); if (!isValidVersion || !isValidConstraintVersion) { logger.once.debug( { packageName: config.packageName, versioning: constraintVersioningName, constraint: releaseConstraint, }, 'Invalid release constraint', ); break; } const isEqual = configConstraint === releaseConstraint; logger.trace( { release, versioning: constraintVersioningName, configConstraint, releaseConstraint, }, `applyConstraintsFiltering(${release.version}): ${configConstraint} === ${releaseConstraint}=${isEqual}`, ); if (isEqual) { satisfiesConstraints = true; break; } const isSubset = constraintVersioning.subset?.( configConstraint, releaseConstraint, ); logger.trace( { release, versioning: constraintVersioningName, configConstraint, releaseConstraint, }, `applyConstraintsFiltering(${release.version}): versioning.subset?.(${configConstraint}, ${releaseConstraint})=${isSubset}`, ); if (isSubset) { satisfiesConstraints = true; break; } const doesMatchConfig = constraintVersioning.matches( configConstraint, releaseConstraint, ); logger.trace( { release, versioning: constraintVersioningName, configConstraint, releaseConstraint, }, `applyConstraintsFiltering(${release.version}): versioning.matches(${configConstraint}, ${releaseConstraint})=${doesMatchConfig}`, ); const doesMatchRelease = constraintVersioning.matches( releaseConstraint, configConstraint, ); logger.trace( { release, versioning: constraintVersioningName, configConstraint, releaseConstraint, }, `applyConstraintsFiltering(${release.version}): versioning.matches(${releaseConstraint}, ${configConstraint})=${doesMatchRelease}`, ); if (doesMatchConfig || doesMatchRelease) { satisfiesConstraints = true; break; } } logger.trace( { release, versioning: constraintVersioningName, }, `applyConstraintsFiltering(${release.version}): satisfiesConstraints=${satisfiesConstraints}`, ); if (!satisfiesConstraints) { filteredReleases.push(release.version); return null; } } return release; }); if (filteredReleases.length) { const count = filteredReleases.length; const packageName = config.packageName; const releases = filteredReleases.join(', '); logger.debug( { datasource: config.datasource, packageName: config.packageName, }, `Filtered out ${count} non-matching releases out of ${startingLength} total for ${packageName} due to constraintsFiltering=strict: ${releases}`, ); } return releaseResult; }