Files
minecraft_launcher/src/shared/jdkVersions.ts
claude-bot fefa2dcdd2 feat(server): 권장 JDK 목록을 Adoptium 에서 동적으로 + 자세히 보기(스냅샷)
- 하드코딩 [8,11,17,21,25] 대신 Adoptium(Temurin) API 에서 실제 배포 버전을 가져와
  편집기 드롭다운을 채운다. 브라우저 CORS 차단이라 서버가 프록시:
  GET /op/jdk-versions (메이저 목록), GET /op/jdk-versions/:major (GA/EA 릴리스명).
  6h 캐시 + 네트워크 실패 시 정적 폴백.
- 편집기: 기본은 LTS(25/21/17/11/8)만 표시, "자세히 보기" 체크 시 비-LTS 메이저까지
  펼치고 선택 메이저의 GA/EA 스냅샷 릴리스명을 함께 보여준다.
- normalizeRecommendedJdk 를 고정 목록 대신 8~99 범위로 완화(동적 메이저 허용).
- 버전 0.4.7→0.4.8.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-17 14:43:55 +09:00

119 lines
4.2 KiB
TypeScript

import https from 'node:https'
// Adoptium(Temurin) API 로부터 실제 배포되는 JDK 버전을 가져온다. 설치기가 이 벤더를
// 쓰므로 사이트의 "권장 JDK" 목록도 같은 소스에서 채운다(하드코딩 금지). 네트워크
// 실패 시에는 정적 폴백을 돌려줘 편집기가 항상 동작하도록 한다.
const AVAILABLE_URL = 'https://api.adoptium.net/v3/info/available_releases'
const CACHE_TTL_MS = 6 * 60 * 60 * 1000 // 6시간
const FALLBACK_MAJORS = [8, 11, 17, 21, 25]
const FALLBACK_LTS = [8, 11, 17, 21, 25]
export interface JdkAvailability {
/** 배포되는 모든 메이저(예: 8,11,17,21,25,26). 내림차순. */
available: number[]
/** LTS 메이저만(예: 8,11,17,21,25). 내림차순. */
lts: number[]
mostRecentLts: number
mostRecentFeature: number
}
export interface JdkReleaseNames {
major: number
/** 정식(GA) 릴리스 이름들(예: jdk-25.0.3+9). */
ga: string[]
/** 개발 스냅샷(EA) 릴리스 이름들(예: jdk-25.0.4+6-ea-beta). */
ea: string[]
}
let availCache: { data: JdkAvailability; at: number } | null = null
const releaseNamesCache = new Map<number, { data: JdkReleaseNames; at: number }>()
function fetchJson<T>(url: string): Promise<T> {
return new Promise((resolve, reject) => {
const request = https.get(url, { timeout: 8000, headers: { accept: 'application/json' } }, (response) => {
const code = response.statusCode ?? 0
if (code !== 200) {
response.resume()
reject(new Error(`Adoptium HTTP ${code}`))
return
}
const chunks: Buffer[] = []
response.on('data', (c: Buffer) => chunks.push(c))
response.on('end', () => {
try {
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')) as T)
} catch (error) {
reject(error as Error)
}
})
})
request.on('error', reject)
request.on('timeout', () => request.destroy(new Error('Adoptium timeout')))
})
}
const sortDesc = (nums: number[]): number[] => [...new Set(nums)].sort((a, b) => b - a)
export async function fetchJdkAvailability(): Promise<JdkAvailability> {
if (availCache && Date.now() - availCache.at < CACHE_TTL_MS) return availCache.data
try {
const raw = await fetchJson<{
available_releases?: number[]
available_lts_releases?: number[]
most_recent_lts?: number
most_recent_feature_release?: number
}>(AVAILABLE_URL)
const available = sortDesc(
(Array.isArray(raw.available_releases) ? raw.available_releases : FALLBACK_MAJORS).filter((n) => Number.isInteger(n))
)
const lts = sortDesc(
(Array.isArray(raw.available_lts_releases) ? raw.available_lts_releases : FALLBACK_LTS).filter((n) => Number.isInteger(n))
)
const data: JdkAvailability = {
available,
lts,
mostRecentLts: Number(raw.most_recent_lts) || (lts[0] ?? 21),
mostRecentFeature: Number(raw.most_recent_feature_release) || (available[0] ?? 25)
}
availCache = { data, at: Date.now() }
return data
} catch {
return (
availCache?.data ?? {
available: sortDesc(FALLBACK_MAJORS),
lts: sortDesc(FALLBACK_LTS),
mostRecentLts: 25,
mostRecentFeature: 25
}
)
}
}
async function fetchReleaseNames(major: number, type: 'ga' | 'ea'): Promise<string[]> {
const url =
`https://api.adoptium.net/v3/assets/feature_releases/${major}/${type}` +
'?architecture=x64&image_type=jdk&os=windows&vendor=eclipse&page_size=10'
try {
const arr = await fetchJson<Array<{ release_name?: string }>>(url)
if (!Array.isArray(arr)) return []
const names: string[] = []
for (const r of arr) {
if (typeof r?.release_name === 'string' && !names.includes(r.release_name)) names.push(r.release_name)
}
return names
} catch {
return []
}
}
export async function fetchJdkReleaseNames(major: number): Promise<JdkReleaseNames> {
const cached = releaseNamesCache.get(major)
if (cached && Date.now() - cached.at < CACHE_TTL_MS) return cached.data
const [ga, ea] = await Promise.all([fetchReleaseNames(major, 'ga'), fetchReleaseNames(major, 'ea')])
const data: JdkReleaseNames = { major, ga, ea }
releaseNamesCache.set(major, { data, at: Date.now() })
return data
}