import { reduce, sortBy } from 'lodash-es' import { allVersions } from './all-versions' import versionSatisfiesRange from './version-satisfies-range' import { next, nextNext } from './enterprise-server-releases' import { getDeepDataByLanguage } from '@/data-directory/lib/get-data' import type { Version } from '@/types' interface VersionsObject { [key: string]: string | string[] } interface GetApplicableVersionsOptions { doNotThrow?: boolean includeNextVersion?: boolean } interface FeatureData { [featureName: string]: { versions: VersionsObject } } // Feature data is dynamically loaded from YAML files let featureData: FeatureData | null = null const allVersionKeys = Object.keys(allVersions) // return an array of versions that an article's product versions encompasses function getApplicableVersions( versionsObj: VersionsObject | string | undefined, filepath?: string, opts: GetApplicableVersionsOptions = {}, ): string[] { if (typeof versionsObj === 'undefined') { throw new Error(`No \`versions\` frontmatter found in ${filepath || 'undefined'}`) } // Catch an old frontmatter value that was used to indicate an article was available in all versions. if (versionsObj === '*') { throw new Error( `${filepath || 'undefined'} contains the invalid versions frontmatter: *. Please explicitly list out all the versions that apply to this article.`, ) } if (!featureData) { featureData = getDeepDataByLanguage('features', 'en') } // Check for frontmatter that includes a feature name, like: // fpt: '*' // feature: 'foo' // or multiple feature names, like: // fpt: '*' // feature: ['foo', 'bar'] // and add the versions affiliated with the feature (e.g., foo) to the frontmatter versions object: // fpt: '*' // ghes: '>=2.23' // where the feature is bringing the ghes versions into the mix. const featureVersionsObj: VersionsObject = typeof versionsObj === 'string' ? {} : reduce( versionsObj, (result: VersionsObject, value, key) => { if (key === 'feature') { if (typeof value === 'string') { Object.assign(result, { ...featureData?.[value]?.versions }) } else if (Array.isArray(value)) { for (const str of value) { Object.assign(result, { ...featureData?.[str]?.versions }) } } delete result[key] } return result }, {}, ) // Get available versions for feature and standard versions. const foundFeatureVersions = evaluateVersions(featureVersionsObj) const foundStandardVersions = typeof versionsObj === 'string' ? [] : evaluateVersions(versionsObj) // Combine them! const applicableVersions: string[] = Array.from( new Set(foundStandardVersions.concat(foundFeatureVersions)), ) if (!applicableVersions.length && !opts.doNotThrow) { throw new Error( `${filepath || 'undefined'} is not available in any currently supported version. Make sure the \`versions\` property includes at least one supported version.`, ) } // Sort them by the order in lib/all-versions. let sortedVersions = sortBy(applicableVersions, (v) => { return allVersionKeys.indexOf(v) }) // Strip out not-yet-supported versions if the option to include them is not provided. if (!opts.includeNextVersion) { sortedVersions = sortedVersions.filter( (v: string) => !(v.endsWith(`@${next}`) || v.endsWith(`@${nextNext}`)), ) } return sortedVersions } function evaluateVersions(versionsObj: VersionsObject): string[] { // get an array like: [ 'free-pro-team@latest', 'enterprise-server@2.21', 'enterprise-cloud@latest' ] const versions: string[] = [] // where versions obj is something like: // fpt: '*' // ghes: '>=2.19' // ghec: '*' // ^ where each key corresponds to a plan's short name (defined in lib/all-versions.ts) for (const [plan, planValue] of Object.entries(versionsObj)) { // Skip non-string plan values for semantic comparison if (typeof planValue !== 'string') continue // For each available plan (e.g., `ghes`), get the matching versions from allVersions. // This will be an array of one or more version objects. const matchingVersionObjs: Version[] = Object.values(allVersions).filter( (relevantVersionObj: Version) => relevantVersionObj.plan === plan || relevantVersionObj.shortName === plan, ) // For each matching version found above, compare it to the provided planValue. // E.g., compare `enterprise-server@2.19` to `ghes: >=2.19`. for (const relevantVersionObj of matchingVersionObjs) { // If the version doesn't require any semantic comparison, we can assume it applies. if (!relevantVersionObj.hasNumberedReleases) { versions.push(relevantVersionObj.version) continue } // Special handling for a plan value that evaluates to the next GHES release number or a hardcoded `next`. // Note these will not be included in the final array unless the `includeNextVersion` option is provided. if (versionSatisfiesRange(next, planValue) || planValue === 'next') { versions.push(`${relevantVersionObj.plan}@${next}`) } if (versionSatisfiesRange(nextNext, planValue)) { versions.push(`${relevantVersionObj.plan}@${nextNext}`) } // Determine which release to use for semantic comparison. const releaseToCompare: string = relevantVersionObj.currentRelease if (releaseToCompare && versionSatisfiesRange(releaseToCompare, planValue)) { versions.push(relevantVersionObj.version) } } } return versions } export default getApplicableVersions