Migrate 6 files from JavaScript to TypeScript (#57885)
This commit is contained in:
@@ -1,7 +1,26 @@
|
||||
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
|
||||
const mockTocItems = [
|
||||
const mockTocItems: TocItem[] = [
|
||||
{
|
||||
title: 'Test Debug Article',
|
||||
intro: 'A test article for debugging functionality.',
|
||||
@@ -20,19 +39,22 @@ const mockTocItems = [
|
||||
]
|
||||
|
||||
// Helper function to simulate the spotlight processing logic from CategoryLanding
|
||||
function processSpotlight(spotlight, tocItems) {
|
||||
const findArticleData = (articlePath) => {
|
||||
const cleanPath = articlePath.startsWith('/') ? articlePath.slice(1) : articlePath
|
||||
function processSpotlight(
|
||||
spotlight: SpotlightItem[] | undefined,
|
||||
tocItems: TocItem[],
|
||||
): ProcessedSpotlightItem[] {
|
||||
const findArticleData = (articlePath: string): TocItem | undefined => {
|
||||
const cleanPath: string = articlePath.startsWith('/') ? articlePath.slice(1) : articlePath
|
||||
return tocItems.find(
|
||||
(item) =>
|
||||
(item: TocItem) =>
|
||||
item.fullPath?.endsWith(cleanPath) ||
|
||||
item.fullPath?.includes(cleanPath.split('/').pop() || ''),
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
spotlight?.map((spotlightItem) => {
|
||||
const articleData = findArticleData(spotlightItem.article)
|
||||
spotlight?.map((spotlightItem: SpotlightItem): ProcessedSpotlightItem => {
|
||||
const articleData: TocItem | undefined = findArticleData(spotlightItem.article)
|
||||
return {
|
||||
article: spotlightItem.article,
|
||||
title: articleData?.title || 'Unknown Article',
|
||||
@@ -46,7 +68,7 @@ function processSpotlight(spotlight, tocItems) {
|
||||
|
||||
describe('spotlight processing logic', () => {
|
||||
test('processes spotlight object items correctly', () => {
|
||||
const spotlight = [
|
||||
const spotlight: SpotlightItem[] = [
|
||||
{
|
||||
article: '/debugging-errors/test-debug-article',
|
||||
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[0]).toEqual({
|
||||
@@ -77,7 +99,7 @@ describe('spotlight processing logic', () => {
|
||||
})
|
||||
|
||||
test('processes multiple spotlight items with different images', () => {
|
||||
const spotlight = [
|
||||
const spotlight: SpotlightItem[] = [
|
||||
{
|
||||
article: '/debugging-errors/test-debug-article',
|
||||
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[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', () => {
|
||||
const spotlight = [
|
||||
const spotlight: SpotlightItem[] = [
|
||||
{
|
||||
article: 'test-debug-article',
|
||||
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].url).toBe('/en/category/debugging-errors/test-debug-article')
|
||||
@@ -116,7 +138,7 @@ describe('spotlight processing logic', () => {
|
||||
})
|
||||
|
||||
test('handles articles not found in tocItems', () => {
|
||||
const spotlight = [
|
||||
const spotlight: SpotlightItem[] = [
|
||||
{
|
||||
article: '/completely/nonexistent/path',
|
||||
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[0]).toEqual({
|
||||
@@ -147,13 +169,13 @@ describe('spotlight processing logic', () => {
|
||||
})
|
||||
|
||||
test('handles empty spotlight array', () => {
|
||||
const spotlight = []
|
||||
const result = processSpotlight(spotlight, mockTocItems)
|
||||
const spotlight: SpotlightItem[] = []
|
||||
const result: ProcessedSpotlightItem[] = processSpotlight(spotlight, mockTocItems)
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
test('handles undefined spotlight', () => {
|
||||
const result = processSpotlight(undefined, mockTocItems)
|
||||
const result: ProcessedSpotlightItem[] = processSpotlight(undefined, mockTocItems)
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -19,7 +19,7 @@ export default async function whatsNewChangelog(
|
||||
const changelogVersions = getApplicableVersions(req.context.page.changelog.versions)
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ export default async function processLearningTracks(
|
||||
const trackVersions = getApplicableVersions(track.versions)
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,33 @@ import {
|
||||
getLocalizedGroupNames,
|
||||
} 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('createOcticonToNameMap', () => {
|
||||
test('creates correct mapping from childGroups', () => {
|
||||
const mockChildGroups = [
|
||||
const mockChildGroups: MockProductGroupData[] = [
|
||||
{ name: 'Get started', octicon: 'RocketIcon', children: ['get-started'] },
|
||||
{ name: 'GitHub Copilot', octicon: 'CopilotIcon', children: ['copilot'] },
|
||||
{ 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['CopilotIcon']).toBe('GitHub Copilot')
|
||||
@@ -24,14 +41,17 @@ describe('get-product-groups helper functions', () => {
|
||||
})
|
||||
|
||||
test('handles missing octicon or name gracefully', () => {
|
||||
const mockChildGroups = [
|
||||
const mockChildGroups: PartialProductGroupData[] = [
|
||||
{ name: 'Valid Group', octicon: 'RocketIcon', children: [] },
|
||||
{ octicon: 'MissingNameIcon', children: [] }, // missing name
|
||||
{ name: 'Missing Octicon', children: [] }, // missing octicon
|
||||
{ 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['MissingNameIcon']).toBeUndefined()
|
||||
@@ -42,19 +62,23 @@ describe('get-product-groups helper functions', () => {
|
||||
|
||||
describe('mapEnglishToLocalizedNames', () => {
|
||||
test('maps English names to localized names using octicon as key', () => {
|
||||
const englishGroups = [
|
||||
const englishGroups: MockProductGroupData[] = [
|
||||
{ name: 'Get started', octicon: 'RocketIcon', children: [] },
|
||||
{ name: 'Security', octicon: 'ShieldLockIcon', children: [] },
|
||||
{ name: 'GitHub Copilot', octicon: 'CopilotIcon', children: [] },
|
||||
]
|
||||
|
||||
const localizedByOcticon = {
|
||||
const localizedByOcticon: { [key: string]: string } = {
|
||||
RocketIcon: 'Empezar',
|
||||
ShieldLockIcon: 'Seguridad',
|
||||
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['Security']).toBe('Seguridad')
|
||||
@@ -63,18 +87,22 @@ describe('get-product-groups helper functions', () => {
|
||||
})
|
||||
|
||||
test('handles missing translations gracefully', () => {
|
||||
const englishGroups = [
|
||||
const englishGroups: MockProductGroupData[] = [
|
||||
{ name: 'Get started', octicon: 'RocketIcon', children: [] },
|
||||
{ name: 'Missing Translation', octicon: 'MissingIcon', children: [] },
|
||||
{ name: 'No Octicon', children: [] },
|
||||
]
|
||||
|
||||
const localizedByOcticon = {
|
||||
const localizedByOcticon: { [key: string]: string } = {
|
||||
RocketIcon: 'Empezar',
|
||||
// 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['Missing Translation']).toBeUndefined()
|
||||
@@ -84,18 +112,22 @@ describe('get-product-groups helper functions', () => {
|
||||
|
||||
test('handles different ordering between English and localized groups', () => {
|
||||
// English groups in one order
|
||||
const englishGroups = [
|
||||
const englishGroups: MockProductGroupData[] = [
|
||||
{ name: 'Get started', octicon: 'RocketIcon', children: [] },
|
||||
{ name: 'Security', octicon: 'ShieldLockIcon', children: [] },
|
||||
]
|
||||
|
||||
// Localized groups in different order (but mapped by octicon)
|
||||
const localizedByOcticon = {
|
||||
const localizedByOcticon: { [key: string]: string } = {
|
||||
ShieldLockIcon: 'Seguridad', // Security comes first in localized
|
||||
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
|
||||
expect(nameMap['Get started']).toBe('Empezar')
|
||||
@@ -105,17 +137,20 @@ describe('get-product-groups helper functions', () => {
|
||||
|
||||
describe('getLocalizedGroupNames integration', () => {
|
||||
test('returns empty object for English language', async () => {
|
||||
const result = await getLocalizedGroupNames('en')
|
||||
const result: { [key: string]: string } = await getLocalizedGroupNames('en')
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
|
||||
test('returns empty object when no translation root available', () => {
|
||||
// Test the fallback when translation root is not found
|
||||
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 result = translationRoot
|
||||
const translationRoot: string | undefined = languages[lang]?.dir
|
||||
const result: { [key: string]: string } = translationRoot
|
||||
? {
|
||||
/* would proceed */
|
||||
}
|
||||
@@ -126,7 +161,7 @@ describe('get-product-groups helper functions', () => {
|
||||
|
||||
test('handles file read errors gracefully', () => {
|
||||
// Test the try/catch behavior when file read fails
|
||||
let result
|
||||
let result: { [key: string]: string }
|
||||
try {
|
||||
// Simulate file read error
|
||||
throw new Error('File not found')
|
||||
@@ -141,28 +176,35 @@ describe('get-product-groups helper functions', () => {
|
||||
describe('full translation pipeline', () => {
|
||||
test('complete flow from English groups to localized names', () => {
|
||||
// Simulate the complete flow
|
||||
const englishChildGroups = [
|
||||
const englishChildGroups: MockProductGroupData[] = [
|
||||
{ name: 'Get started', octicon: 'RocketIcon', children: ['get-started'] },
|
||||
{ name: 'Security', octicon: 'ShieldLockIcon', children: ['code-security'] },
|
||||
{ name: 'GitHub Copilot', octicon: 'CopilotIcon', children: ['copilot'] },
|
||||
]
|
||||
|
||||
// Simulate what would come from a Spanish localized file
|
||||
const mockLocalizedChildGroups = [
|
||||
const mockLocalizedChildGroups: MockProductGroupData[] = [
|
||||
{ name: 'Empezar', octicon: 'RocketIcon', children: ['get-started'] },
|
||||
{ name: 'Seguridad', octicon: 'ShieldLockIcon', children: ['code-security'] },
|
||||
{ name: 'GitHub Copilot', octicon: 'CopilotIcon', children: ['copilot'] },
|
||||
]
|
||||
|
||||
// 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
|
||||
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
|
||||
const finalResult = englishChildGroups.map((group) => {
|
||||
const localizedName = localizedNames[group.name] || group.name
|
||||
const finalResult = englishChildGroups.map((group: MockProductGroupData) => {
|
||||
const localizedName: string = localizedNames[group.name] || group.name
|
||||
return {
|
||||
name: localizedName,
|
||||
octicon: group.octicon,
|
||||
@@ -31,7 +31,7 @@ export default async function secretScanning(
|
||||
const { currentVersion } = req.context
|
||||
|
||||
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
|
||||
|
||||
@@ -5,7 +5,20 @@ const releasePattern = '[a-z0-9-.]+'
|
||||
const delimiter = '@'
|
||||
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',
|
||||
additionalProperties: false,
|
||||
required: [
|
||||
@@ -106,3 +119,5 @@ export default {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default schema
|
||||
@@ -3,7 +3,26 @@ import semver from 'semver'
|
||||
|
||||
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
|
||||
@@ -91,7 +110,7 @@ export const latestStable = releaseCandidate ? supported[1] : latest
|
||||
export const oldestSupported = supported[supported.length - 1]
|
||||
|
||||
// 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]) => [
|
||||
version,
|
||||
{
|
||||
@@ -100,11 +119,13 @@ export const dates = Object.fromEntries(
|
||||
displayReleaseDate: processDateForDisplay(versionData.generalAvailabilityDate),
|
||||
},
|
||||
]),
|
||||
)
|
||||
) as Record<string, EnhancedVersionDateData>
|
||||
|
||||
// Deprecation tracking
|
||||
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
|
||||
export const deprecatedOnNewSite = deprecated.filter((version) =>
|
||||
@@ -133,7 +154,7 @@ export const deprecatedReleasesOnDeveloperSite = deprecated.filter((version) =>
|
||||
* @param {string|null} date - ISO date string
|
||||
* @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
|
||||
const currentTimestamp = Math.floor(Date.now() / 1000)
|
||||
const dateTimestamp = Math.floor(new Date(date).getTime() / 1000)
|
||||
@@ -146,24 +167,24 @@ function processDateForDisplay(date) {
|
||||
* @param {string} v2 - Next version
|
||||
* @throws {Error} If version sequence is invalid
|
||||
*/
|
||||
function isValidNext(v1, v2) {
|
||||
const semverV1 = semver.coerce(v1).raw
|
||||
const semverV2 = semver.coerce(v2).raw
|
||||
function isValidNext(v1: string, v2: string): void {
|
||||
const semverV1 = semver.coerce(v1)!.raw
|
||||
const semverV2 = semver.coerce(v2)!.raw
|
||||
const isValid =
|
||||
semverV2 === semver.inc(semverV1, 'minor') || semverV2 === semver.inc(semverV1, 'major')
|
||||
if (!isValid)
|
||||
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)
|
||||
}
|
||||
|
||||
export const getNextReleaseNumber = (releaseNum) => {
|
||||
export const getNextReleaseNumber = (releaseNum: string): string => {
|
||||
return all[findReleaseNumberIndex(releaseNum) - 1]
|
||||
}
|
||||
|
||||
export const getPreviousReleaseNumber = (releaseNum) => {
|
||||
export const getPreviousReleaseNumber = (releaseNum: string): string => {
|
||||
return all[findReleaseNumberIndex(releaseNum) + 1]
|
||||
}
|
||||
|
||||
@@ -180,6 +201,7 @@ export default {
|
||||
nextNext,
|
||||
supported,
|
||||
deprecated,
|
||||
deprecatedWithFunctionalRedirects,
|
||||
legacyAssetVersions,
|
||||
all,
|
||||
latest,
|
||||
@@ -193,11 +215,13 @@ export default {
|
||||
firstVersionDeprecatedOnNewSite,
|
||||
lastVersionWithoutArchivedRedirectsFile,
|
||||
lastReleaseWithLegacyFormat,
|
||||
firstReleaseStoredInBlobStorage,
|
||||
deprecatedReleasesWithLegacyFormat,
|
||||
deprecatedReleasesWithNewFormat,
|
||||
deprecatedReleasesOnDeveloperSite,
|
||||
firstReleaseNote,
|
||||
firstRestoredAdminGuides,
|
||||
findReleaseNumberIndex,
|
||||
getNextReleaseNumber,
|
||||
getPreviousReleaseNumber,
|
||||
}
|
||||
@@ -3,21 +3,36 @@ 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'
|
||||
|
||||
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)
|
||||
|
||||
// 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') {
|
||||
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.
|
||||
if (versionsObj === '*') {
|
||||
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,34 +50,39 @@ function getApplicableVersions(versionsObj, filepath, opts = {}) {
|
||||
// fpt: '*'
|
||||
// ghes: '>=2.23'
|
||||
// where the feature is bringing the ghes versions into the mix.
|
||||
const featureVersionsObj = reduce(
|
||||
versionsObj,
|
||||
(result, value, key) => {
|
||||
if (key === 'feature') {
|
||||
if (typeof value === 'string') {
|
||||
Object.assign(result, { ...featureData[value]?.versions })
|
||||
} else if (Array.isArray(value)) {
|
||||
value.forEach((str) => {
|
||||
Object.assign(result, { ...featureData[str].versions })
|
||||
})
|
||||
}
|
||||
delete result[key]
|
||||
}
|
||||
return result
|
||||
},
|
||||
{},
|
||||
)
|
||||
const featureVersionsObj: VersionsObject =
|
||||
typeof versionsObj === 'string'
|
||||
? {}
|
||||
: reduce(
|
||||
versionsObj,
|
||||
(result: any, value, key) => {
|
||||
if (key === 'feature') {
|
||||
if (typeof value === 'string') {
|
||||
Object.assign(result, { ...featureData[value]?.versions })
|
||||
} else if (Array.isArray(value)) {
|
||||
value.forEach((str) => {
|
||||
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 = evaluateVersions(versionsObj)
|
||||
const foundStandardVersions = typeof versionsObj === 'string' ? [] : evaluateVersions(versionsObj)
|
||||
|
||||
// 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) {
|
||||
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.
|
||||
if (!opts.includeNextVersion) {
|
||||
sortedVersions = sortedVersions.filter(
|
||||
(v) => !(v.endsWith(`@${next}`) || v.endsWith(`@${nextNext}`)),
|
||||
(v: string) => !(v.endsWith(`@${next}`) || v.endsWith(`@${nextNext}`)),
|
||||
)
|
||||
}
|
||||
|
||||
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' ]
|
||||
const versions = []
|
||||
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.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.
|
||||
// This will be an array of one or more version objects.
|
||||
const matchingVersionObjs = Object.values(allVersions).filter(
|
||||
(relevantVersionObj) =>
|
||||
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`.
|
||||
matchingVersionObjs.forEach((relevantVersionObj) => {
|
||||
matchingVersionObjs.forEach((relevantVersionObj: Version) => {
|
||||
// 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)
|
||||
return
|
||||
}
|
||||
@@ -117,11 +140,9 @@ function evaluateVersions(versionsObj) {
|
||||
}
|
||||
|
||||
// Determine which release to use for semantic comparison.
|
||||
const releaseToCompare = relevantVersionObj.hasNumberedReleases
|
||||
? relevantVersionObj.currentRelease
|
||||
: relevantVersionObj.internalLatestRelease
|
||||
const releaseToCompare: string = relevantVersionObj.currentRelease
|
||||
|
||||
if (versionSatisfiesRange(releaseToCompare, planValue)) {
|
||||
if (releaseToCompare && versionSatisfiesRange(releaseToCompare, planValue)) {
|
||||
versions.push(relevantVersionObj.version)
|
||||
}
|
||||
})
|
||||
@@ -10,7 +10,7 @@ import { deprecated, oldestSupported } from '@/versions/lib/enterprise-server-re
|
||||
const allVersionKeys = Object.values(allVersions)
|
||||
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), {
|
||||
includeBasePath: true,
|
||||
directories: false,
|
||||
@@ -20,7 +20,24 @@ const walkFiles = (pathToWalk, ext) => {
|
||||
const markdownFiles = walkFiles('content', '.md').concat(walkFiles('data', '.md'))
|
||||
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
|
||||
'==': '=',
|
||||
ver_gt: '>',
|
||||
@@ -50,9 +67,10 @@ async function main() {
|
||||
const newContent = makeLiquidReplacements(contentReplacements, content)
|
||||
|
||||
// B. UPDATE FRONTMATTER VERSIONS PROPERTY
|
||||
const { data } = frontmatter(newContent)
|
||||
const { data } = frontmatter(newContent) as { data: VersionData }
|
||||
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
|
||||
const valueToUse = value
|
||||
.replace('2.23', '3.0')
|
||||
@@ -68,15 +86,16 @@ async function main() {
|
||||
console.error(`can't find supported version for ${plan}`)
|
||||
process.exit(1)
|
||||
}
|
||||
delete data.versions[plan]
|
||||
data.versions[versionObj.shortName] = valueToUse
|
||||
delete versions[plan]
|
||||
versions[versionObj.shortName] = valueToUse
|
||||
})
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
console.log(contentReplacements)
|
||||
} 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.
|
||||
function removeInputProps(arrayOfObjects) {
|
||||
return arrayOfObjects.map((obj) => {
|
||||
// Using any for token objects as liquidjs doesn't provide TypeScript types
|
||||
function removeInputProps(arrayOfObjects: any[]): any[] {
|
||||
return arrayOfObjects.map((obj: any) => {
|
||||
delete obj.input || delete obj.token.input
|
||||
return obj
|
||||
})
|
||||
}
|
||||
|
||||
function makeLiquidReplacements(replacementsObj, text) {
|
||||
function makeLiquidReplacements(replacementsObj: ReplacementsMap, text: string): string {
|
||||
let newText = text
|
||||
Object.entries(replacementsObj).forEach(([oldCond, newCond]) => {
|
||||
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_lt "myVersion@myRelease -> ifversion myVersionShort < myRelease
|
||||
// if enterpriseServerVersions contains currentVersion -> ifversion ghes
|
||||
function getLiquidReplacements(content, file) {
|
||||
const replacements = {}
|
||||
function getLiquidReplacements(content: string, file: string): ReplacementsMap {
|
||||
const replacements: ReplacementsMap = {}
|
||||
|
||||
const tokenizer = new Tokenizer(content)
|
||||
const tokens = removeInputProps(tokenizer.readTopLevelTokens())
|
||||
@@ -157,7 +177,7 @@ function getLiquidReplacements(content, file) {
|
||||
token
|
||||
.replace(/(if|elsif) /, '')
|
||||
.split(/ (or|and) /)
|
||||
.forEach((op) => {
|
||||
.forEach((op: any) => {
|
||||
if (op === 'or' || op === 'and') {
|
||||
newToken.push(op)
|
||||
return
|
||||
@@ -193,7 +213,7 @@ function getLiquidReplacements(content, file) {
|
||||
|
||||
// Handle numbered releases!
|
||||
if (versionObj.hasNumberedReleases) {
|
||||
const newOperator = operatorsMap[operator]
|
||||
const newOperator: string | undefined = operatorsMap[operator]
|
||||
if (!newOperator) {
|
||||
console.error(
|
||||
`Couldn't find an operator that corresponds to ${operator} in "${token} in "${file}`,
|
||||
Reference in New Issue
Block a user