packages/jsii-pacmak/lib/targets/version-utils.ts (199 lines of code) (raw):

import { Comparator, Range, parse } from 'semver'; import { inspect } from 'util'; import { TargetName } from '.'; /** * Converts a SemVer range expression to a Maven version range expression. * * @param semverRange the SemVer range expression to convert. * @param suffix the suffix to add to versions in the range. * * @see https://cwiki.apache.org/confluence/display/MAVENOLD/Dependency+Mediation+and+Conflict+Resolution */ export function toMavenVersionRange( semverRange: string, suffix?: string, ): string { return toBracketNotation(semverRange, suffix, { semver: false, target: TargetName.JAVA, }); } /** * Converts a SemVer range expression to a NuGet version range expression. * * @param semverRange the SemVer range expression to convert. * * @see https://docs.microsoft.com/en-us/nuget/concepts/package-versioning#version-ranges-and-wildcards */ export function toNuGetVersionRange(semverRange: string): string { return toBracketNotation(semverRange, undefined, { semver: false, target: TargetName.DOTNET, }); } /** * Converts a SemVer range expression to a Python setuptools compatible version * constraint expression. * * @param semverRange the SemVer range expression to convert. */ export function toPythonVersionRange(semverRange: string): string { const range = new Range(semverRange); return range.set .map((set) => set .map((comp) => { const versionId = toReleaseVersion( comp.semver.raw?.replace(/-0$/, '') ?? '0.0.0', TargetName.PYTHON, ); // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check switch (comp.operator) { case '': // With ^0.0.0, somehow we get a left entry with an empty operator and value, we'll fix this up return comp.value === '' ? '>=0.0.0' : `==${versionId}`; case '=': return `==${versionId}`; default: // >, >=, <, <= are all valid expressions return `${comp.operator}${versionId}`; } }) .join(', '), ) .join(', '); } /** * Converts an original version number from the NPM convention to the target * language's convention for expressing the same. For versions that do not * include a prerelease identifier, this always returns the assembly version * unmodified. * * @param assemblyVersion the assembly version being released * @param target the target language for which the version is destined * * @returns the version that should be serialized */ export function toReleaseVersion( assemblyVersion: string, target: TargetName, ): string { const version = parse(assemblyVersion); if (version == null) { throw new Error( `Unable to parse the provided assembly version: "${assemblyVersion}"`, ); } if (version.prerelease.length === 0) { return assemblyVersion; } switch (target) { case TargetName.PYTHON: const baseVersion = `${version.major}.${version.minor}.${version.patch}`; // Python supports a limited set of identifiers... And we have a mapping table... // https://packaging.python.org/guides/distributing-packages-using-setuptools/#pre-release-versioning const releaseLabels: Record<string, string> = { alpha: 'a', beta: 'b', rc: 'rc', post: 'post', dev: 'dev', pre: 'pre', }; const validationErrors: string[] = []; // Ensure that prerelease composed entirely of [label, sequence] pairs version.prerelease.forEach((elem, idx, arr) => { const next: string | number | undefined = arr[idx + 1]; if (typeof elem === 'string') { if (!Object.keys(releaseLabels).includes(elem)) { validationErrors.push( `Label ${elem} is not one of ${Object.keys(releaseLabels).join( ',', )}`, ); } if (next === undefined || !Number.isInteger(next)) { validationErrors.push( `Label ${elem} must be followed by a positive integer`, ); } } }); if (validationErrors.length > 0) { throw new Error( `Unable to map prerelease identifier (in: ${assemblyVersion}) components to python: ${inspect( version.prerelease, )}. The format should be 'X.Y.Z-[label.sequence][.post.sequence][.(dev|pre).sequence]', where sequence is a positive integer and label is one of ${inspect( Object.keys(releaseLabels), )}. Validation errors encountered: ${validationErrors.join(', ')}`, ); } // PEP440 supports multiple labels in a given version, so // we should attempt to identify and map as many labels as // possible from the given prerelease input // e.g. 1.2.3-rc.123.dev.456.post.789 => 1.2.3.rc123.dev456.post789 const postIdx = version.prerelease.findIndex( (v) => v.toString() === 'post', ); const devIdx = version.prerelease.findIndex((v) => ['dev', 'pre'].includes(v.toString()), ); const preReleaseIdx = version.prerelease.findIndex((v) => ['alpha', 'beta', 'rc'].includes(v.toString()), ); const prereleaseVersion = [ preReleaseIdx > -1 ? `${releaseLabels[version.prerelease[preReleaseIdx]]}${ version.prerelease[preReleaseIdx + 1] ?? 0 }` : undefined, postIdx > -1 ? `post${version.prerelease[postIdx + 1] ?? 0}` : undefined, devIdx > -1 ? `dev${version.prerelease[devIdx + 1] ?? 0}` : undefined, ] .filter((v) => v) .join('.'); return version.build.length > 0 ? `${baseVersion}.${prereleaseVersion}+${version.build.join('.')}` : `${baseVersion}.${prereleaseVersion}`; case TargetName.DOTNET: case TargetName.GO: case TargetName.JAVA: case TargetName.JAVASCRIPT: // Not touching - the NPM version number should be usable as-is break; } return assemblyVersion; } /** * Converts a semantic version range to the kind of bracket notation used by * Maven and NuGet. For example, this turns `^1.2.3` into `[1.2.3,2.0.0)`. * * @param semverRange The semantic version range to be converted. * @param suffix A version suffix to apply to versions in the resulting expression. * @param semver Whether the target supports full semantic versioning (including * `-0` as the lowest possible prerelease identifier) * * @returns a bracket-notation version range. */ function toBracketNotation( semverRange: string, suffix?: string, { semver = true, target = TargetName.JAVASCRIPT, }: { semver?: boolean; target?: TargetName } = {}, ): string { if (semverRange === '*' || semverRange === '^0.0.0') { semverRange = '>=0.0.0'; } const range = new Range(semverRange); return range.set .map((set) => { if (set.length === 1) { const version = set[0].semver.raw; if (!version && range.raw === '>=0.0.0') { // Case where version is '*' return `[0.0.0,)`; } switch (set[0].operator || '=') { // "[version]" => means exactly version case '=': return `[${addSuffix(version)}]`; // "(version,]" => means greater than version case '>': return `(${addSuffix(version)},)`; // "[version,]" => means greater than or equal to that version case '>=': return `[${addSuffix(version)},)`; // "[,version)" => means less than version case '<': return `(,${addSuffix(version, !semver)})`; // "[,version]" => means less than or equal to version case '<=': return `(,${addSuffix(version)}]`; } } else if (set.length === 2) { const nugetRange = toBracketRange(set[0], set[1]); if (nugetRange) { return nugetRange; } } throw new Error( `Unsupported SemVer range set in ${semverRange}: ${set .map((comp) => comp.value) .join(', ')}`, ); }) .join(', '); function toBracketRange( left: Comparator, right: Comparator, ): string | undefined { if (left.operator.startsWith('<') && right.operator.startsWith('>')) { // Order isn't ideal, swap around.. [left, right] = [right, left]; } // With ^0.0.0, somehow we get a left entry with an empty operator and value, we'll fix this up if (left.operator === '' && left.value === '') { left = new Comparator('>=0.0.0', left.options); } if (!left.operator.startsWith('>') || !right.operator.startsWith('<')) { // We only support ranges defined like "> (or >=) left, < (or <=) right" return undefined; } const leftBrace = left.operator.endsWith('=') ? '[' : '('; const rightBrace = right.operator.endsWith('=') ? ']' : ')'; return `${leftBrace}${addSuffix(left.semver.raw)},${addSuffix( right.semver.raw, right.operator === '<' && !semver, )}${rightBrace}`; } function addSuffix(str: string | undefined, trimDashZero = false) { if (!str) { return ''; } if (trimDashZero) { str = str.replace(/-0$/, ''); } return suffix ? `${str}${suffix}` : toReleaseVersion(str, target); } }