1
0
mirror of synced 2025-12-19 18:10:59 -05:00

Migrate 6 files from JavaScript to TypeScript (#57885)

This commit is contained in:
Kevin Heis
2025-10-09 09:19:30 -07:00
committed by GitHub
parent 328c2f69d8
commit 6d3f74a01c
9 changed files with 251 additions and 107 deletions

View File

@@ -1,7 +1,26 @@
import { describe, expect, test } from 'vitest' import { describe, expect, test } from 'vitest'
interface TocItem {
title: string
intro: string
fullPath?: string
}
interface SpotlightItem {
article: string
image: string
}
interface ProcessedSpotlightItem {
article: string
title: string
description: string
url: string
image: string
}
// Mock data to simulate tocItems and spotlight configurations // Mock data to simulate tocItems and spotlight configurations
const mockTocItems = [ const mockTocItems: TocItem[] = [
{ {
title: 'Test Debug Article', title: 'Test Debug Article',
intro: 'A test article for debugging functionality.', intro: 'A test article for debugging functionality.',
@@ -20,19 +39,22 @@ const mockTocItems = [
] ]
// Helper function to simulate the spotlight processing logic from CategoryLanding // Helper function to simulate the spotlight processing logic from CategoryLanding
function processSpotlight(spotlight, tocItems) { function processSpotlight(
const findArticleData = (articlePath) => { spotlight: SpotlightItem[] | undefined,
const cleanPath = articlePath.startsWith('/') ? articlePath.slice(1) : articlePath tocItems: TocItem[],
): ProcessedSpotlightItem[] {
const findArticleData = (articlePath: string): TocItem | undefined => {
const cleanPath: string = articlePath.startsWith('/') ? articlePath.slice(1) : articlePath
return tocItems.find( return tocItems.find(
(item) => (item: TocItem) =>
item.fullPath?.endsWith(cleanPath) || item.fullPath?.endsWith(cleanPath) ||
item.fullPath?.includes(cleanPath.split('/').pop() || ''), item.fullPath?.includes(cleanPath.split('/').pop() || ''),
) )
} }
return ( return (
spotlight?.map((spotlightItem) => { spotlight?.map((spotlightItem: SpotlightItem): ProcessedSpotlightItem => {
const articleData = findArticleData(spotlightItem.article) const articleData: TocItem | undefined = findArticleData(spotlightItem.article)
return { return {
article: spotlightItem.article, article: spotlightItem.article,
title: articleData?.title || 'Unknown Article', title: articleData?.title || 'Unknown Article',
@@ -46,7 +68,7 @@ function processSpotlight(spotlight, tocItems) {
describe('spotlight processing logic', () => { describe('spotlight processing logic', () => {
test('processes spotlight object items correctly', () => { test('processes spotlight object items correctly', () => {
const spotlight = [ const spotlight: SpotlightItem[] = [
{ {
article: '/debugging-errors/test-debug-article', article: '/debugging-errors/test-debug-article',
image: '/assets/images/test-debugging.png', image: '/assets/images/test-debugging.png',
@@ -57,7 +79,7 @@ describe('spotlight processing logic', () => {
}, },
] ]
const result = processSpotlight(spotlight, mockTocItems) const result: ProcessedSpotlightItem[] = processSpotlight(spotlight, mockTocItems)
expect(result).toHaveLength(2) expect(result).toHaveLength(2)
expect(result[0]).toEqual({ expect(result[0]).toEqual({
@@ -77,7 +99,7 @@ describe('spotlight processing logic', () => {
}) })
test('processes multiple spotlight items with different images', () => { test('processes multiple spotlight items with different images', () => {
const spotlight = [ const spotlight: SpotlightItem[] = [
{ {
article: '/debugging-errors/test-debug-article', article: '/debugging-errors/test-debug-article',
image: '/assets/images/debugging.png', image: '/assets/images/debugging.png',
@@ -92,7 +114,7 @@ describe('spotlight processing logic', () => {
}, },
] ]
const result = processSpotlight(spotlight, mockTocItems) const result: ProcessedSpotlightItem[] = processSpotlight(spotlight, mockTocItems)
expect(result).toHaveLength(3) expect(result).toHaveLength(3)
expect(result[0].image).toBe('/assets/images/debugging.png') expect(result[0].image).toBe('/assets/images/debugging.png')
@@ -102,13 +124,13 @@ describe('spotlight processing logic', () => {
}) })
test('finds articles by filename when full path does not match', () => { test('finds articles by filename when full path does not match', () => {
const spotlight = [ const spotlight: SpotlightItem[] = [
{ {
article: 'test-debug-article', article: 'test-debug-article',
image: '/assets/images/debug.png', image: '/assets/images/debug.png',
}, },
] ]
const result = processSpotlight(spotlight, mockTocItems) const result: ProcessedSpotlightItem[] = processSpotlight(spotlight, mockTocItems)
expect(result[0].title).toBe('Test Debug Article') expect(result[0].title).toBe('Test Debug Article')
expect(result[0].url).toBe('/en/category/debugging-errors/test-debug-article') expect(result[0].url).toBe('/en/category/debugging-errors/test-debug-article')
@@ -116,7 +138,7 @@ describe('spotlight processing logic', () => {
}) })
test('handles articles not found in tocItems', () => { test('handles articles not found in tocItems', () => {
const spotlight = [ const spotlight: SpotlightItem[] = [
{ {
article: '/completely/nonexistent/path', article: '/completely/nonexistent/path',
image: '/assets/images/missing1.png', image: '/assets/images/missing1.png',
@@ -127,7 +149,7 @@ describe('spotlight processing logic', () => {
}, },
] ]
const result = processSpotlight(spotlight, mockTocItems) const result: ProcessedSpotlightItem[] = processSpotlight(spotlight, mockTocItems)
expect(result).toHaveLength(2) expect(result).toHaveLength(2)
expect(result[0]).toEqual({ expect(result[0]).toEqual({
@@ -147,13 +169,13 @@ describe('spotlight processing logic', () => {
}) })
test('handles empty spotlight array', () => { test('handles empty spotlight array', () => {
const spotlight = [] const spotlight: SpotlightItem[] = []
const result = processSpotlight(spotlight, mockTocItems) const result: ProcessedSpotlightItem[] = processSpotlight(spotlight, mockTocItems)
expect(result).toEqual([]) expect(result).toEqual([])
}) })
test('handles undefined spotlight', () => { test('handles undefined spotlight', () => {
const result = processSpotlight(undefined, mockTocItems) const result: ProcessedSpotlightItem[] = processSpotlight(undefined, mockTocItems)
expect(result).toEqual([]) expect(result).toEqual([])
}) })
}) })

View File

@@ -19,7 +19,7 @@ export default async function whatsNewChangelog(
const changelogVersions = getApplicableVersions(req.context.page.changelog.versions) const changelogVersions = getApplicableVersions(req.context.page.changelog.versions)
// If the current version is not included, do not display a changelog. // If the current version is not included, do not display a changelog.
if (!changelogVersions.includes(req.context.currentVersion)) { if (!req.context.currentVersion || !changelogVersions.includes(req.context.currentVersion)) {
return next() return next()
} }
} }

View File

@@ -82,7 +82,7 @@ export default async function processLearningTracks(
const trackVersions = getApplicableVersions(track.versions) const trackVersions = getApplicableVersions(track.versions)
// If the current version is not included, do not display the track. // If the current version is not included, do not display the track.
if (!trackVersions.includes(context.currentVersion)) { if (!context.currentVersion || !trackVersions.includes(context.currentVersion)) {
continue continue
} }
} }

View File

@@ -6,16 +6,33 @@ import {
getLocalizedGroupNames, getLocalizedGroupNames,
} from '@/products/lib/get-product-groups' } from '@/products/lib/get-product-groups'
// Mock data interface for tests - uses required name to match library expectations
interface MockProductGroupData {
name: string
octicon?: string
children: string[]
}
// Mock data for testing edge cases with optional fields
interface PartialProductGroupData {
name?: string
octicon?: string
children: string[]
}
describe('get-product-groups helper functions', () => { describe('get-product-groups helper functions', () => {
describe('createOcticonToNameMap', () => { describe('createOcticonToNameMap', () => {
test('creates correct mapping from childGroups', () => { test('creates correct mapping from childGroups', () => {
const mockChildGroups = [ const mockChildGroups: MockProductGroupData[] = [
{ name: 'Get started', octicon: 'RocketIcon', children: ['get-started'] }, { name: 'Get started', octicon: 'RocketIcon', children: ['get-started'] },
{ name: 'GitHub Copilot', octicon: 'CopilotIcon', children: ['copilot'] }, { name: 'GitHub Copilot', octicon: 'CopilotIcon', children: ['copilot'] },
{ name: 'Security', octicon: 'ShieldLockIcon', children: ['code-security'] }, { name: 'Security', octicon: 'ShieldLockIcon', children: ['code-security'] },
] ]
const octiconToName = createOcticonToNameMap(mockChildGroups) // Using any to cast mock data structure to match library's expected ProductGroupData type
const octiconToName: { [key: string]: string } = createOcticonToNameMap(
mockChildGroups as any,
)
expect(octiconToName['RocketIcon']).toBe('Get started') expect(octiconToName['RocketIcon']).toBe('Get started')
expect(octiconToName['CopilotIcon']).toBe('GitHub Copilot') expect(octiconToName['CopilotIcon']).toBe('GitHub Copilot')
@@ -24,14 +41,17 @@ describe('get-product-groups helper functions', () => {
}) })
test('handles missing octicon or name gracefully', () => { test('handles missing octicon or name gracefully', () => {
const mockChildGroups = [ const mockChildGroups: PartialProductGroupData[] = [
{ name: 'Valid Group', octicon: 'RocketIcon', children: [] }, { name: 'Valid Group', octicon: 'RocketIcon', children: [] },
{ octicon: 'MissingNameIcon', children: [] }, // missing name { octicon: 'MissingNameIcon', children: [] }, // missing name
{ name: 'Missing Octicon', children: [] }, // missing octicon { name: 'Missing Octicon', children: [] }, // missing octicon
{ name: '', octicon: 'EmptyNameIcon', children: [] }, // empty name { name: '', octicon: 'EmptyNameIcon', children: [] }, // empty name
] ]
const octiconToName = createOcticonToNameMap(mockChildGroups) // Using any to test edge cases with partial/missing fields that wouldn't normally pass strict typing
const octiconToName: { [key: string]: string } = createOcticonToNameMap(
mockChildGroups as any,
)
expect(octiconToName['RocketIcon']).toBe('Valid Group') expect(octiconToName['RocketIcon']).toBe('Valid Group')
expect(octiconToName['MissingNameIcon']).toBeUndefined() expect(octiconToName['MissingNameIcon']).toBeUndefined()
@@ -42,19 +62,23 @@ describe('get-product-groups helper functions', () => {
describe('mapEnglishToLocalizedNames', () => { describe('mapEnglishToLocalizedNames', () => {
test('maps English names to localized names using octicon as key', () => { test('maps English names to localized names using octicon as key', () => {
const englishGroups = [ const englishGroups: MockProductGroupData[] = [
{ name: 'Get started', octicon: 'RocketIcon', children: [] }, { name: 'Get started', octicon: 'RocketIcon', children: [] },
{ name: 'Security', octicon: 'ShieldLockIcon', children: [] }, { name: 'Security', octicon: 'ShieldLockIcon', children: [] },
{ name: 'GitHub Copilot', octicon: 'CopilotIcon', children: [] }, { name: 'GitHub Copilot', octicon: 'CopilotIcon', children: [] },
] ]
const localizedByOcticon = { const localizedByOcticon: { [key: string]: string } = {
RocketIcon: 'Empezar', RocketIcon: 'Empezar',
ShieldLockIcon: 'Seguridad', ShieldLockIcon: 'Seguridad',
CopilotIcon: 'GitHub Copilot', // Some names stay the same CopilotIcon: 'GitHub Copilot', // Some names stay the same
} }
const nameMap = mapEnglishToLocalizedNames(englishGroups, localizedByOcticon) // Using any to cast mock data structure to match library's expected ProductGroupData type
const nameMap: { [key: string]: string } = mapEnglishToLocalizedNames(
englishGroups as any,
localizedByOcticon,
)
expect(nameMap['Get started']).toBe('Empezar') expect(nameMap['Get started']).toBe('Empezar')
expect(nameMap['Security']).toBe('Seguridad') expect(nameMap['Security']).toBe('Seguridad')
@@ -63,18 +87,22 @@ describe('get-product-groups helper functions', () => {
}) })
test('handles missing translations gracefully', () => { test('handles missing translations gracefully', () => {
const englishGroups = [ const englishGroups: MockProductGroupData[] = [
{ name: 'Get started', octicon: 'RocketIcon', children: [] }, { name: 'Get started', octicon: 'RocketIcon', children: [] },
{ name: 'Missing Translation', octicon: 'MissingIcon', children: [] }, { name: 'Missing Translation', octicon: 'MissingIcon', children: [] },
{ name: 'No Octicon', children: [] }, { name: 'No Octicon', children: [] },
] ]
const localizedByOcticon = { const localizedByOcticon: { [key: string]: string } = {
RocketIcon: 'Empezar', RocketIcon: 'Empezar',
// MissingIcon is not in the localized map // MissingIcon is not in the localized map
} }
const nameMap = mapEnglishToLocalizedNames(englishGroups, localizedByOcticon) // Using any to cast mock data structure to match library's expected ProductGroupData type
const nameMap: { [key: string]: string } = mapEnglishToLocalizedNames(
englishGroups as any,
localizedByOcticon,
)
expect(nameMap['Get started']).toBe('Empezar') expect(nameMap['Get started']).toBe('Empezar')
expect(nameMap['Missing Translation']).toBeUndefined() expect(nameMap['Missing Translation']).toBeUndefined()
@@ -84,18 +112,22 @@ describe('get-product-groups helper functions', () => {
test('handles different ordering between English and localized groups', () => { test('handles different ordering between English and localized groups', () => {
// English groups in one order // English groups in one order
const englishGroups = [ const englishGroups: MockProductGroupData[] = [
{ name: 'Get started', octicon: 'RocketIcon', children: [] }, { name: 'Get started', octicon: 'RocketIcon', children: [] },
{ name: 'Security', octicon: 'ShieldLockIcon', children: [] }, { name: 'Security', octicon: 'ShieldLockIcon', children: [] },
] ]
// Localized groups in different order (but mapped by octicon) // Localized groups in different order (but mapped by octicon)
const localizedByOcticon = { const localizedByOcticon: { [key: string]: string } = {
ShieldLockIcon: 'Seguridad', // Security comes first in localized ShieldLockIcon: 'Seguridad', // Security comes first in localized
RocketIcon: 'Empezar', // Get started comes second RocketIcon: 'Empezar', // Get started comes second
} }
const nameMap = mapEnglishToLocalizedNames(englishGroups, localizedByOcticon) // Using any to cast mock data structure to match library's expected ProductGroupData type
const nameMap: { [key: string]: string } = mapEnglishToLocalizedNames(
englishGroups as any,
localizedByOcticon,
)
// Should correctly map regardless of order // Should correctly map regardless of order
expect(nameMap['Get started']).toBe('Empezar') expect(nameMap['Get started']).toBe('Empezar')
@@ -105,17 +137,20 @@ describe('get-product-groups helper functions', () => {
describe('getLocalizedGroupNames integration', () => { describe('getLocalizedGroupNames integration', () => {
test('returns empty object for English language', async () => { test('returns empty object for English language', async () => {
const result = await getLocalizedGroupNames('en') const result: { [key: string]: string } = await getLocalizedGroupNames('en')
expect(result).toEqual({}) expect(result).toEqual({})
}) })
test('returns empty object when no translation root available', () => { test('returns empty object when no translation root available', () => {
// Test the fallback when translation root is not found // Test the fallback when translation root is not found
const lang = 'unknown-lang' const lang = 'unknown-lang'
const languages = { en: { dir: '/en' }, es: { dir: '/es' } } const languages: { [key: string]: { dir: string } } = {
en: { dir: '/en' },
es: { dir: '/es' },
}
const translationRoot = languages[lang]?.dir const translationRoot: string | undefined = languages[lang]?.dir
const result = translationRoot const result: { [key: string]: string } = translationRoot
? { ? {
/* would proceed */ /* would proceed */
} }
@@ -126,7 +161,7 @@ describe('get-product-groups helper functions', () => {
test('handles file read errors gracefully', () => { test('handles file read errors gracefully', () => {
// Test the try/catch behavior when file read fails // Test the try/catch behavior when file read fails
let result let result: { [key: string]: string }
try { try {
// Simulate file read error // Simulate file read error
throw new Error('File not found') throw new Error('File not found')
@@ -141,28 +176,35 @@ describe('get-product-groups helper functions', () => {
describe('full translation pipeline', () => { describe('full translation pipeline', () => {
test('complete flow from English groups to localized names', () => { test('complete flow from English groups to localized names', () => {
// Simulate the complete flow // Simulate the complete flow
const englishChildGroups = [ const englishChildGroups: MockProductGroupData[] = [
{ name: 'Get started', octicon: 'RocketIcon', children: ['get-started'] }, { name: 'Get started', octicon: 'RocketIcon', children: ['get-started'] },
{ name: 'Security', octicon: 'ShieldLockIcon', children: ['code-security'] }, { name: 'Security', octicon: 'ShieldLockIcon', children: ['code-security'] },
{ name: 'GitHub Copilot', octicon: 'CopilotIcon', children: ['copilot'] }, { name: 'GitHub Copilot', octicon: 'CopilotIcon', children: ['copilot'] },
] ]
// Simulate what would come from a Spanish localized file // Simulate what would come from a Spanish localized file
const mockLocalizedChildGroups = [ const mockLocalizedChildGroups: MockProductGroupData[] = [
{ name: 'Empezar', octicon: 'RocketIcon', children: ['get-started'] }, { name: 'Empezar', octicon: 'RocketIcon', children: ['get-started'] },
{ name: 'Seguridad', octicon: 'ShieldLockIcon', children: ['code-security'] }, { name: 'Seguridad', octicon: 'ShieldLockIcon', children: ['code-security'] },
{ name: 'GitHub Copilot', octicon: 'CopilotIcon', children: ['copilot'] }, { name: 'GitHub Copilot', octicon: 'CopilotIcon', children: ['copilot'] },
] ]
// Step 1: Create octicon -> localized name mapping // Step 1: Create octicon -> localized name mapping
const localizedByOcticon = createOcticonToNameMap(mockLocalizedChildGroups) // Using any to cast mock data structure to match library's expected ProductGroupData type
const localizedByOcticon: { [key: string]: string } = createOcticonToNameMap(
mockLocalizedChildGroups as any,
)
// Step 2: Map English names to localized names // Step 2: Map English names to localized names
const localizedNames = mapEnglishToLocalizedNames(englishChildGroups, localizedByOcticon) // Using any to cast mock data structure to match library's expected ProductGroupData type
const localizedNames: { [key: string]: string } = mapEnglishToLocalizedNames(
englishChildGroups as any,
localizedByOcticon,
)
// Step 3: Use in final mapping // Step 3: Use in final mapping
const finalResult = englishChildGroups.map((group) => { const finalResult = englishChildGroups.map((group: MockProductGroupData) => {
const localizedName = localizedNames[group.name] || group.name const localizedName: string = localizedNames[group.name] || group.name
return { return {
name: localizedName, name: localizedName,
octicon: group.octicon, octicon: group.octicon,

View File

@@ -31,7 +31,7 @@ export default async function secretScanning(
const { currentVersion } = req.context const { currentVersion } = req.context
req.context.secretScanningData = secretScanningData.filter((entry) => req.context.secretScanningData = secretScanningData.filter((entry) =>
getApplicableVersions(entry.versions).includes(currentVersion), currentVersion ? getApplicableVersions(entry.versions).includes(currentVersion) : false,
) )
// Some entries might use Liquid syntax, so we need // Some entries might use Liquid syntax, so we need

View File

@@ -5,7 +5,20 @@ const releasePattern = '[a-z0-9-.]+'
const delimiter = '@' const delimiter = '@'
const versionPattern = `${planPattern}${delimiter}${releasePattern}` const versionPattern = `${planPattern}${delimiter}${releasePattern}`
export default { interface VersionSchema {
type: string
additionalProperties: boolean
required: string[]
properties: {
[key: string]: {
description: string
type: string
pattern?: string
}
}
}
const schema: VersionSchema = {
type: 'object', type: 'object',
additionalProperties: false, additionalProperties: false,
required: [ required: [
@@ -106,3 +119,5 @@ export default {
}, },
}, },
} }
export default schema

View File

@@ -3,7 +3,26 @@ import semver from 'semver'
import versionSatisfiesRange from './version-satisfies-range' import versionSatisfiesRange from './version-satisfies-range'
const rawDates = JSON.parse(fs.readFileSync('src/ghes-releases/lib/enterprise-dates.json', 'utf8')) interface VersionDateData {
releaseDate: string
releaseCandidateDate?: string
generalAvailabilityDate?: string
deprecationDate: string
[key: string]: any
}
interface EnhancedVersionDateData extends VersionDateData {
displayCandidateDate?: string | null
displayReleaseDate?: string | null
}
interface RawDatesData {
[version: string]: VersionDateData
}
const rawDates: RawDatesData = JSON.parse(
fs.readFileSync('src/ghes-releases/lib/enterprise-dates.json', 'utf8'),
)
// ============================================================================ // ============================================================================
// STATICALLY DEFINED VALUES // STATICALLY DEFINED VALUES
@@ -91,7 +110,7 @@ export const latestStable = releaseCandidate ? supported[1] : latest
export const oldestSupported = supported[supported.length - 1] export const oldestSupported = supported[supported.length - 1]
// Enhanced dates object with computed display values for templates // Enhanced dates object with computed display values for templates
export const dates = Object.fromEntries( export const dates: Record<string, EnhancedVersionDateData> = Object.fromEntries(
Object.entries(rawDates).map(([version, versionData]) => [ Object.entries(rawDates).map(([version, versionData]) => [
version, version,
{ {
@@ -100,11 +119,13 @@ export const dates = Object.fromEntries(
displayReleaseDate: processDateForDisplay(versionData.generalAvailabilityDate), displayReleaseDate: processDateForDisplay(versionData.generalAvailabilityDate),
}, },
]), ]),
) ) as Record<string, EnhancedVersionDateData>
// Deprecation tracking // Deprecation tracking
export const nextDeprecationDate = dates[oldestSupported].deprecationDate export const nextDeprecationDate = dates[oldestSupported].deprecationDate
export const isOldestReleaseDeprecated = new Date() > new Date(nextDeprecationDate) export const isOldestReleaseDeprecated = nextDeprecationDate
? new Date() > new Date(nextDeprecationDate)
: false
// Filtered version arrays for different use cases // Filtered version arrays for different use cases
export const deprecatedOnNewSite = deprecated.filter((version) => export const deprecatedOnNewSite = deprecated.filter((version) =>
@@ -133,7 +154,7 @@ export const deprecatedReleasesOnDeveloperSite = deprecated.filter((version) =>
* @param {string|null} date - ISO date string * @param {string|null} date - ISO date string
* @returns {string|null} - Date string if in the past, null if future or invalid * @returns {string|null} - Date string if in the past, null if future or invalid
*/ */
function processDateForDisplay(date) { function processDateForDisplay(date: string | undefined): string | null {
if (!date) return null if (!date) return null
const currentTimestamp = Math.floor(Date.now() / 1000) const currentTimestamp = Math.floor(Date.now() / 1000)
const dateTimestamp = Math.floor(new Date(date).getTime() / 1000) const dateTimestamp = Math.floor(new Date(date).getTime() / 1000)
@@ -146,24 +167,24 @@ function processDateForDisplay(date) {
* @param {string} v2 - Next version * @param {string} v2 - Next version
* @throws {Error} If version sequence is invalid * @throws {Error} If version sequence is invalid
*/ */
function isValidNext(v1, v2) { function isValidNext(v1: string, v2: string): void {
const semverV1 = semver.coerce(v1).raw const semverV1 = semver.coerce(v1)!.raw
const semverV2 = semver.coerce(v2).raw const semverV2 = semver.coerce(v2)!.raw
const isValid = const isValid =
semverV2 === semver.inc(semverV1, 'minor') || semverV2 === semver.inc(semverV1, 'major') semverV2 === semver.inc(semverV1, 'minor') || semverV2 === semver.inc(semverV1, 'major')
if (!isValid) if (!isValid)
throw new Error(`The version "${v2}" is not one version ahead of "${v1}" as expected`) throw new Error(`The version "${v2}" is not one version ahead of "${v1}" as expected`)
} }
export const findReleaseNumberIndex = (releaseNum) => { export const findReleaseNumberIndex = (releaseNum: string): number => {
return all.findIndex((i) => i === releaseNum) return all.findIndex((i) => i === releaseNum)
} }
export const getNextReleaseNumber = (releaseNum) => { export const getNextReleaseNumber = (releaseNum: string): string => {
return all[findReleaseNumberIndex(releaseNum) - 1] return all[findReleaseNumberIndex(releaseNum) - 1]
} }
export const getPreviousReleaseNumber = (releaseNum) => { export const getPreviousReleaseNumber = (releaseNum: string): string => {
return all[findReleaseNumberIndex(releaseNum) + 1] return all[findReleaseNumberIndex(releaseNum) + 1]
} }
@@ -180,6 +201,7 @@ export default {
nextNext, nextNext,
supported, supported,
deprecated, deprecated,
deprecatedWithFunctionalRedirects,
legacyAssetVersions, legacyAssetVersions,
all, all,
latest, latest,
@@ -193,11 +215,13 @@ export default {
firstVersionDeprecatedOnNewSite, firstVersionDeprecatedOnNewSite,
lastVersionWithoutArchivedRedirectsFile, lastVersionWithoutArchivedRedirectsFile,
lastReleaseWithLegacyFormat, lastReleaseWithLegacyFormat,
firstReleaseStoredInBlobStorage,
deprecatedReleasesWithLegacyFormat, deprecatedReleasesWithLegacyFormat,
deprecatedReleasesWithNewFormat, deprecatedReleasesWithNewFormat,
deprecatedReleasesOnDeveloperSite, deprecatedReleasesOnDeveloperSite,
firstReleaseNote, firstReleaseNote,
firstRestoredAdminGuides, firstRestoredAdminGuides,
findReleaseNumberIndex,
getNextReleaseNumber, getNextReleaseNumber,
getPreviousReleaseNumber, getPreviousReleaseNumber,
} }

View File

@@ -3,21 +3,36 @@ import { allVersions } from './all-versions'
import versionSatisfiesRange from './version-satisfies-range' import versionSatisfiesRange from './version-satisfies-range'
import { next, nextNext } from './enterprise-server-releases' import { next, nextNext } from './enterprise-server-releases'
import { getDeepDataByLanguage } from '@/data-directory/lib/get-data' import { getDeepDataByLanguage } from '@/data-directory/lib/get-data'
import type { Version } from '@/types'
let featureData = null interface VersionsObject {
[key: string]: string | string[]
}
interface GetApplicableVersionsOptions {
doNotThrow?: boolean
includeNextVersion?: boolean
}
// Using any for feature data as it's dynamically loaded from YAML files
let featureData: any = null
const allVersionKeys = Object.keys(allVersions) const allVersionKeys = Object.keys(allVersions)
// return an array of versions that an article's product versions encompasses // return an array of versions that an article's product versions encompasses
function getApplicableVersions(versionsObj, filepath, opts = {}) { function getApplicableVersions(
versionsObj: VersionsObject | string | undefined,
filepath?: string,
opts: GetApplicableVersionsOptions = {},
): string[] {
if (typeof versionsObj === 'undefined') { if (typeof versionsObj === 'undefined') {
throw new Error(`No \`versions\` frontmatter found in ${filepath}`) 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. // Catch an old frontmatter value that was used to indicate an article was available in all versions.
if (versionsObj === '*') { if (versionsObj === '*') {
throw new Error( throw new Error(
`${filepath} contains the invalid versions frontmatter: *. Please explicitly list out all the versions that apply to this article.`, `${filepath || 'undefined'} contains the invalid versions frontmatter: *. Please explicitly list out all the versions that apply to this article.`,
) )
} }
@@ -35,9 +50,12 @@ function getApplicableVersions(versionsObj, filepath, opts = {}) {
// fpt: '*' // fpt: '*'
// ghes: '>=2.23' // ghes: '>=2.23'
// where the feature is bringing the ghes versions into the mix. // where the feature is bringing the ghes versions into the mix.
const featureVersionsObj = reduce( const featureVersionsObj: VersionsObject =
typeof versionsObj === 'string'
? {}
: reduce(
versionsObj, versionsObj,
(result, value, key) => { (result: any, value, key) => {
if (key === 'feature') { if (key === 'feature') {
if (typeof value === 'string') { if (typeof value === 'string') {
Object.assign(result, { ...featureData[value]?.versions }) Object.assign(result, { ...featureData[value]?.versions })
@@ -55,14 +73,16 @@ function getApplicableVersions(versionsObj, filepath, opts = {}) {
// Get available versions for feature and standard versions. // Get available versions for feature and standard versions.
const foundFeatureVersions = evaluateVersions(featureVersionsObj) const foundFeatureVersions = evaluateVersions(featureVersionsObj)
const foundStandardVersions = evaluateVersions(versionsObj) const foundStandardVersions = typeof versionsObj === 'string' ? [] : evaluateVersions(versionsObj)
// Combine them! // Combine them!
const applicableVersions = Array.from(new Set(foundStandardVersions.concat(foundFeatureVersions))) const applicableVersions: string[] = Array.from(
new Set(foundStandardVersions.concat(foundFeatureVersions)),
)
if (!applicableVersions.length && !opts.doNotThrow) { if (!applicableVersions.length && !opts.doNotThrow) {
throw new Error( throw new Error(
`${filepath} is not available in any currently supported version. Make sure the \`versions\` property includes at least one supported version.`, `${filepath || 'undefined'} is not available in any currently supported version. Make sure the \`versions\` property includes at least one supported version.`,
) )
} }
@@ -74,35 +94,38 @@ function getApplicableVersions(versionsObj, filepath, opts = {}) {
// Strip out not-yet-supported versions if the option to include them is not provided. // Strip out not-yet-supported versions if the option to include them is not provided.
if (!opts.includeNextVersion) { if (!opts.includeNextVersion) {
sortedVersions = sortedVersions.filter( sortedVersions = sortedVersions.filter(
(v) => !(v.endsWith(`@${next}`) || v.endsWith(`@${nextNext}`)), (v: string) => !(v.endsWith(`@${next}`) || v.endsWith(`@${nextNext}`)),
) )
} }
return sortedVersions return sortedVersions
} }
function evaluateVersions(versionsObj) { function evaluateVersions(versionsObj: VersionsObject): string[] {
// get an array like: [ 'free-pro-team@latest', 'enterprise-server@2.21', 'enterprise-cloud@latest' ] // get an array like: [ 'free-pro-team@latest', 'enterprise-server@2.21', 'enterprise-cloud@latest' ]
const versions = [] const versions: string[] = []
// where versions obj is something like: // where versions obj is something like:
// fpt: '*' // fpt: '*'
// ghes: '>=2.19' // ghes: '>=2.19'
// ghec: '*' // ghec: '*'
// ^ where each key corresponds to a plan's short name (defined in lib/all-versions.js) // ^ where each key corresponds to a plan's short name (defined in lib/all-versions.js)
Object.entries(versionsObj).forEach(([plan, planValue]) => { Object.entries(versionsObj).forEach(([plan, planValue]: [string, string | string[]]) => {
// Skip non-string plan values for semantic comparison
if (typeof planValue !== 'string') return
// For each available plan (e.g., `ghes`), get the matching versions from allVersions. // For each available plan (e.g., `ghes`), get the matching versions from allVersions.
// This will be an array of one or more version objects. // This will be an array of one or more version objects.
const matchingVersionObjs = Object.values(allVersions).filter( const matchingVersionObjs: Version[] = Object.values(allVersions).filter(
(relevantVersionObj) => (relevantVersionObj: Version) =>
relevantVersionObj.plan === plan || relevantVersionObj.shortName === plan, relevantVersionObj.plan === plan || relevantVersionObj.shortName === plan,
) )
// For each matching version found above, compare it to the provided planValue. // For each matching version found above, compare it to the provided planValue.
// E.g., compare `enterprise-server@2.19` to `ghes: >=2.19`. // E.g., compare `enterprise-server@2.19` to `ghes: >=2.19`.
matchingVersionObjs.forEach((relevantVersionObj) => { matchingVersionObjs.forEach((relevantVersionObj: Version) => {
// If the version doesn't require any semantic comparison, we can assume it applies. // If the version doesn't require any semantic comparison, we can assume it applies.
if (!(relevantVersionObj.hasNumberedReleases || relevantVersionObj.internalLatestRelease)) { if (!relevantVersionObj.hasNumberedReleases) {
versions.push(relevantVersionObj.version) versions.push(relevantVersionObj.version)
return return
} }
@@ -117,11 +140,9 @@ function evaluateVersions(versionsObj) {
} }
// Determine which release to use for semantic comparison. // Determine which release to use for semantic comparison.
const releaseToCompare = relevantVersionObj.hasNumberedReleases const releaseToCompare: string = relevantVersionObj.currentRelease
? relevantVersionObj.currentRelease
: relevantVersionObj.internalLatestRelease
if (versionSatisfiesRange(releaseToCompare, planValue)) { if (releaseToCompare && versionSatisfiesRange(releaseToCompare, planValue)) {
versions.push(relevantVersionObj.version) versions.push(relevantVersionObj.version)
} }
}) })

View File

@@ -10,7 +10,7 @@ import { deprecated, oldestSupported } from '@/versions/lib/enterprise-server-re
const allVersionKeys = Object.values(allVersions) const allVersionKeys = Object.values(allVersions)
const dryRun = ['-d', '--dry-run'].includes(process.argv[2]) const dryRun = ['-d', '--dry-run'].includes(process.argv[2])
const walkFiles = (pathToWalk, ext) => { const walkFiles = (pathToWalk: string, ext: string): string[] => {
return walk(path.posix.join(process.cwd(), pathToWalk), { return walk(path.posix.join(process.cwd(), pathToWalk), {
includeBasePath: true, includeBasePath: true,
directories: false, directories: false,
@@ -20,7 +20,24 @@ const walkFiles = (pathToWalk, ext) => {
const markdownFiles = walkFiles('content', '.md').concat(walkFiles('data', '.md')) const markdownFiles = walkFiles('content', '.md').concat(walkFiles('data', '.md'))
const yamlFiles = walkFiles('data', '.yml') const yamlFiles = walkFiles('data', '.yml')
const operatorsMap = { interface ReplacementsMap {
[key: string]: string
}
interface VersionData {
versions?: Record<string, string> | string
[key: string]: any
}
interface OperatorsMap {
[key: string]: string
'==': string
ver_gt: string
ver_lt: string
'!=': string
}
const operatorsMap: OperatorsMap = {
// old: new // old: new
'==': '=', '==': '=',
ver_gt: '>', ver_gt: '>',
@@ -50,9 +67,10 @@ async function main() {
const newContent = makeLiquidReplacements(contentReplacements, content) const newContent = makeLiquidReplacements(contentReplacements, content)
// B. UPDATE FRONTMATTER VERSIONS PROPERTY // B. UPDATE FRONTMATTER VERSIONS PROPERTY
const { data } = frontmatter(newContent) const { data } = frontmatter(newContent) as { data: VersionData }
if (data.versions && typeof data.versions !== 'string') { if (data.versions && typeof data.versions !== 'string') {
Object.entries(data.versions).forEach(([plan, value]) => { const versions = data.versions as Record<string, string>
Object.entries(versions).forEach(([plan, value]) => {
// Update legacy versioning while we're here // Update legacy versioning while we're here
const valueToUse = value const valueToUse = value
.replace('2.23', '3.0') .replace('2.23', '3.0')
@@ -68,15 +86,16 @@ async function main() {
console.error(`can't find supported version for ${plan}`) console.error(`can't find supported version for ${plan}`)
process.exit(1) process.exit(1)
} }
delete data.versions[plan] delete versions[plan]
data.versions[versionObj.shortName] = valueToUse versions[versionObj.shortName] = valueToUse
}) })
} }
if (dryRun) { if (dryRun) {
console.log(contentReplacements) console.log(contentReplacements)
} else { } else {
fs.writeFileSync(file, frontmatter.stringify(newContent, data, { lineWidth: 10000 })) // Using any for frontmatter.stringify options as gray-matter types don't include lineWidth
fs.writeFileSync(file, frontmatter.stringify(newContent, data, { lineWidth: 10000 } as any))
} }
} }
@@ -109,14 +128,15 @@ main().then(
) )
// Convenience function to help with readability by removing this large but unneded property. // Convenience function to help with readability by removing this large but unneded property.
function removeInputProps(arrayOfObjects) { // Using any for token objects as liquidjs doesn't provide TypeScript types
return arrayOfObjects.map((obj) => { function removeInputProps(arrayOfObjects: any[]): any[] {
return arrayOfObjects.map((obj: any) => {
delete obj.input || delete obj.token.input delete obj.input || delete obj.token.input
return obj return obj
}) })
} }
function makeLiquidReplacements(replacementsObj, text) { function makeLiquidReplacements(replacementsObj: ReplacementsMap, text: string): string {
let newText = text let newText = text
Object.entries(replacementsObj).forEach(([oldCond, newCond]) => { Object.entries(replacementsObj).forEach(([oldCond, newCond]) => {
const oldCondRegex = new RegExp(`({%-?)\\s*?${escapeRegExp(oldCond)}\\s*?(-?%})`, 'g') const oldCondRegex = new RegExp(`({%-?)\\s*?${escapeRegExp(oldCond)}\\s*?(-?%})`, 'g')
@@ -139,8 +159,8 @@ function makeLiquidReplacements(replacementsObj, text) {
// if currentVersion ver_gt "myVersion@myRelease -> ifversion myVersionShort > myRelease // if currentVersion ver_gt "myVersion@myRelease -> ifversion myVersionShort > myRelease
// if currentVersion ver_lt "myVersion@myRelease -> ifversion myVersionShort < myRelease // if currentVersion ver_lt "myVersion@myRelease -> ifversion myVersionShort < myRelease
// if enterpriseServerVersions contains currentVersion -> ifversion ghes // if enterpriseServerVersions contains currentVersion -> ifversion ghes
function getLiquidReplacements(content, file) { function getLiquidReplacements(content: string, file: string): ReplacementsMap {
const replacements = {} const replacements: ReplacementsMap = {}
const tokenizer = new Tokenizer(content) const tokenizer = new Tokenizer(content)
const tokens = removeInputProps(tokenizer.readTopLevelTokens()) const tokens = removeInputProps(tokenizer.readTopLevelTokens())
@@ -157,7 +177,7 @@ function getLiquidReplacements(content, file) {
token token
.replace(/(if|elsif) /, '') .replace(/(if|elsif) /, '')
.split(/ (or|and) /) .split(/ (or|and) /)
.forEach((op) => { .forEach((op: any) => {
if (op === 'or' || op === 'and') { if (op === 'or' || op === 'and') {
newToken.push(op) newToken.push(op)
return return
@@ -193,7 +213,7 @@ function getLiquidReplacements(content, file) {
// Handle numbered releases! // Handle numbered releases!
if (versionObj.hasNumberedReleases) { if (versionObj.hasNumberedReleases) {
const newOperator = operatorsMap[operator] const newOperator: string | undefined = operatorsMap[operator]
if (!newOperator) { if (!newOperator) {
console.error( console.error(
`Couldn't find an operator that corresponds to ${operator} in "${token} in "${file}`, `Couldn't find an operator that corresponds to ${operator} in "${token} in "${file}`,