diff --git a/packages/core/src/awsService/cloudformation/lsp-server/cfnManifest.ts b/packages/core/src/awsService/cloudformation/lsp-server/cfnManifest.ts new file mode 100644 index 00000000000..b986359fde6 --- /dev/null +++ b/packages/core/src/awsService/cloudformation/lsp-server/cfnManifest.ts @@ -0,0 +1,43 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Manifest } from '../../../shared/lsp/types' +import { CfnLspServerEnvType } from './lspServerConfig' +import { CfnLspVersion, mapLegacyLinux, useOldLinuxVersion } from './utils' +import { getLogger } from '../../../shared/logger/logger' + +interface CfnReleaseManifest { + manifestSchemaVersion: string + isManifestDeprecated: boolean + prod: CfnLspVersion[] + beta: CfnLspVersion[] + alpha: CfnLspVersion[] +} + +/** + * Converts the raw CFN release manifest into the shared Manifest type, + * preferring the version flagged as `latest` if it exists. + * Remaps legacy Linux targets when running on older glibc systems. + */ +export function parseCfnManifest(content: string, environment: CfnLspServerEnvType): Manifest { + const raw: CfnReleaseManifest = JSON.parse(content) + let versions: CfnLspVersion[] = raw[environment] ?? [] + + if (useOldLinuxVersion()) { + getLogger('awsCfnLsp').info('In a legacy or sandbox Linux environment') + versions = mapLegacyLinux(versions) + } + + const latestVersion = versions.find((v) => v.latest && !v.isDelisted) + const effectiveVersions = latestVersion ? [latestVersion] : versions + + return { + manifestSchemaVersion: raw.manifestSchemaVersion, + artifactId: 'cloudformation-languageserver', + artifactDescription: 'AWS CloudFormation Language Server', + isManifestDeprecated: raw.isManifestDeprecated, + versions: effectiveVersions, + } +} diff --git a/packages/core/src/awsService/cloudformation/lsp-server/githubManifestAdapter.ts b/packages/core/src/awsService/cloudformation/lsp-server/githubManifestAdapter.ts deleted file mode 100644 index 88dfeb441c2..00000000000 --- a/packages/core/src/awsService/cloudformation/lsp-server/githubManifestAdapter.ts +++ /dev/null @@ -1,201 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { CfnLspName, CfnLspServerEnvType } from './lspServerConfig' -import { - addWindows, - CfnManifest, - CfnTarget, - CfnLspVersion, - dedupeAndGetLatestVersions, - extractPlatformAndArch, - useOldLinuxVersion, - mapLegacyLinux, -} from './utils' -import { getLogger } from '../../../shared/logger/logger' -import { ToolkitError } from '../../../shared/errors' - -export class GitHubManifestAdapter { - constructor( - private readonly repoOwner: string, - private readonly repoName: string, - readonly environment: CfnLspServerEnvType - ) {} - - async getManifest(): Promise { - let manifest: CfnManifest - try { - manifest = await this.getManifestJson() - } catch (err) { - getLogger('awsCfnLsp').error(ToolkitError.chain(err, 'Failed to get CloudFormation manifest')) - manifest = await this.getFromReleases() - } - - getLogger('awsCfnLsp').info( - 'Candidate versions: %s', - manifest.versions - .map( - (v) => - `${v.serverVersion}[${v.targets - .sort() - .map((t) => `${t.platform}-${t.arch}-${t.nodejs}`) - .join(',')}]` - ) - .join(', ') - ) - - if (process.platform !== 'linux') { - return manifest - } - - const useFallbackLinux = useOldLinuxVersion() - if (!useFallbackLinux) { - return manifest - } - - getLogger('awsCfnLsp').info('In a legacy or sandbox Linux environment') - manifest.versions = mapLegacyLinux(manifest.versions) - - getLogger('awsCfnLsp').info( - 'Remapped candidate versions: %s', - manifest.versions - .map( - (v) => - `${v.serverVersion}[${v.targets - .sort() - .map((t) => `${t.platform}-${t.arch}-${t.nodejs}`) - .join(',')}]` - ) - .join(', ') - ) - return manifest - } - - private async getFromReleases(): Promise { - const releases = await this.fetchGitHubReleases() - const envReleases = this.filterByEnvironment(releases) - const sortedReleases = envReleases.sort((a, b) => { - return b.tag_name.localeCompare(a.tag_name) - }) - const versions = dedupeAndGetLatestVersions(sortedReleases.map((release) => this.convertRelease(release))) - getLogger('awsCfnLsp').info( - 'Candidate versions: %s', - versions - .map((v) => `${v.serverVersion}[${v.targets.map((t) => `${t.platform}-${t.arch}`).join(',')}]`) - .join(', ') - ) - return { - manifestSchemaVersion: '1.0', - artifactId: CfnLspName, - artifactDescription: 'GitHub CloudFormation Language Server', - isManifestDeprecated: false, - versions: versions, - } - } - - private filterByEnvironment(releases: GitHubRelease[]): GitHubRelease[] { - return releases.filter((release) => { - const tag = release.tag_name - if (this.environment === 'alpha') { - return release.prerelease && tag.endsWith('-alpha') - } else if (this.environment === 'beta') { - return release.prerelease && tag.endsWith('-beta') - } else { - return !release.prerelease - } - }) - } - - private async fetchGitHubReleases(): Promise { - const response = await fetch(`https://api.github.com/repos/${this.repoOwner}/${this.repoName}/releases`) - if (!response.ok) { - throw new Error(`GitHub API error: ${response.status}`) - } - return response.json() - } - - private convertRelease(release: GitHubRelease): CfnLspVersion { - return { - serverVersion: release.tag_name, - isDelisted: false, - targets: addWindows(this.extractTargets(release.assets)), - } - } - - private extractTargets(assets: GitHubAsset[]): CfnTarget[] { - return assets.map((asset) => { - const { arch, platform, nodejs } = extractPlatformAndArch(asset.name) - - return { - platform, - arch, - nodejs, - contents: [ - { - filename: asset.name, - url: asset.browser_download_url, - hashes: [], - bytes: asset.size, - }, - ], - } - }) - } - - private async getManifestJson(): Promise { - const response = await fetch( - `https://raw.githubusercontent.com/${this.repoOwner}/${this.repoName}/refs/heads/main/assets/release-manifest.json` - ) - if (!response.ok) { - throw new Error(`GitHub API error: ${response.status}`) - } - - const json = (await response.json()) as Record - - return { - manifestSchemaVersion: json.manifestSchemaVersion as string, - artifactId: json.artifactId as string, - artifactDescription: json.artifactDescription as string, - isManifestDeprecated: json.isManifestDeprecated as boolean, - versions: json[this.environment] as CfnLspVersion[], - } - } -} - -/* eslint-disable @typescript-eslint/naming-convention */ -interface GitHubAsset { - url: string - browser_download_url: string - id: number - node_id: string - name: string - label: string | null - state: string - content_type: string - size: number - download_count: number - created_at: string - updated_at: string -} - -interface GitHubRelease { - url: string - html_url: string - assets_url: string - upload_url: string - tarball_url: string | null - zipball_url: string | null - id: number - node_id: string - tag_name: string - target_commitish: string - name: string | null - body: string | null - draft: boolean - prerelease: boolean - created_at: string // ISO 8601 date string - published_at: string | null // ISO 8601 date string - assets: GitHubAsset[] -} diff --git a/packages/core/src/awsService/cloudformation/lsp-server/lspInstaller.ts b/packages/core/src/awsService/cloudformation/lsp-server/lspInstaller.ts index fa54beba44b..8049a03a9cd 100644 --- a/packages/core/src/awsService/cloudformation/lsp-server/lspInstaller.ts +++ b/packages/core/src/awsService/cloudformation/lsp-server/lspInstaller.ts @@ -4,17 +4,20 @@ */ import { BaseLspInstaller } from '../../../shared/lsp/baseLspInstaller' -import { GitHubManifestAdapter } from './githubManifestAdapter' import { fs } from '../../../shared/fs/fs' import { CfnLspName, CfnLspServerEnvType, CfnLspServerFile } from './lspServerConfig' import { isAutomation, isBeta, isDebugInstance } from '../../../shared/vscode/env' -import { dirname, join } from 'path' +import { join } from 'path' import { getLogger } from '../../../shared/logger/logger' -import { ResourcePaths } from '../../../shared/lsp/types' +import { Manifest, ResourcePaths } from '../../../shared/lsp/types' import * as nodeFs from 'fs' // eslint-disable-line no-restricted-imports -import globals from '../../../shared/extensionGlobals' +import { ManifestResolver } from '../../../shared/lsp/manifestResolver' +import { parseCfnManifest } from './cfnManifest' import { toString } from '../utils' +const cfnManifestUrl = + 'https://raw.githubusercontent.com/aws-cloudformation/cloudformation-languageserver/refs/heads/main/assets/release-manifest.json' + function determineEnvironment(): CfnLspServerEnvType { if (isDebugInstance()) { return 'alpha' @@ -24,64 +27,39 @@ function determineEnvironment(): CfnLspServerEnvType { return 'prod' } -export class CfnLspInstaller extends BaseLspInstaller { - private readonly githubManifest = new GitHubManifestAdapter( - 'aws-cloudformation', - 'cloudformation-languageserver', - determineEnvironment() - ) +class CfnManifestResolver extends ManifestResolver { + constructor(private readonly environment: CfnLspServerEnvType) { + super(cfnManifestUrl, CfnLspName, 'cfnLsp') + } + + protected override parseManifest(content: string): Manifest { + getLogger('awsCfnLsp').info(`Parsing CloudFormation LSP manifest for ${this.environment}`) + return parseCfnManifest(content, this.environment) + } +} +export class CfnLspInstaller extends BaseLspInstaller { constructor() { super( { - manifestUrl: 'github', + manifestUrl: cfnManifestUrl, supportedVersions: '<2.0.0', id: CfnLspName, suppressPromptPrefix: 'cfnLsp', }, 'awsCfnLsp', - { - resolve: async () => { - const log = getLogger('awsCfnLsp') - const cfnManifestStorageKey = 'aws.cloudformation.lsp.manifest' - - try { - const manifest = await this.githubManifest.getManifest() - log.info( - `Creating CloudFormation LSP manifest for ${this.githubManifest.environment}`, - manifest.versions.map((v) => v.serverVersion) - ) - - // Cache in CloudFormation-specific global state storage - globals.globalState.tryUpdate(cfnManifestStorageKey, { - content: JSON.stringify(manifest), - }) - - return manifest - } catch (error) { - log.warn(`GitHub fetch failed, trying cached manifest: ${error}`) - - // Try cached manifest from CloudFormation-specific storage - const manifestData = globals.globalState.tryGet(cfnManifestStorageKey, Object, {}) - - if (manifestData?.content) { - log.debug('Using cached manifest for offline mode') - return JSON.parse(manifestData.content) - } - - log.error('No cached manifest found') - throw error - } - }, - } as any, + new CfnManifestResolver(determineEnvironment()), 'sha256' ) } protected async postInstall(assetDirectory: string): Promise { - const resourcePaths = this.resourcePaths(assetDirectory) - const rootDir = dirname(resourcePaths.lsp) - await fs.chmod(join(rootDir, 'bin', process.platform === 'win32' ? 'cfn-init.exe' : 'cfn-init'), 0o755) + const entries = nodeFs.readdirSync(assetDirectory, { withFileTypes: true }) + const folder = entries.find((e) => e.isDirectory()) + if (folder) { + const rootDir = join(assetDirectory, folder.name) + await fs.chmod(join(rootDir, 'bin', process.platform === 'win32' ? 'cfn-init.exe' : 'cfn-init'), 0o755) + } } protected resourcePaths(assetDirectory?: string): ResourcePaths { @@ -92,7 +70,6 @@ export class CfnLspInstaller extends BaseLspInstaller { } } - // Find the single extracted directory const entries = nodeFs.readdirSync(assetDirectory, { withFileTypes: true }) const folders = entries.filter((entry) => entry.isDirectory()) diff --git a/packages/core/src/awsService/cloudformation/lsp-server/utils.ts b/packages/core/src/awsService/cloudformation/lsp-server/utils.ts index 9775fb0036b..84e2bcca1bf 100644 --- a/packages/core/src/awsService/cloudformation/lsp-server/utils.ts +++ b/packages/core/src/awsService/cloudformation/lsp-server/utils.ts @@ -13,111 +13,13 @@ export interface CfnTarget extends Target { nodejs?: string } export interface CfnLspVersion extends LspVersion { + latest?: boolean targets: CfnTarget[] } export interface CfnManifest extends Manifest { versions: CfnLspVersion[] } -export function addWindows(targets: CfnTarget[]): CfnTarget[] { - const win32Targets = targets.filter((target) => { - return target.platform === 'win32' - }) - - const windowsTargets = targets.filter((target) => { - return target.platform === 'windows' - }) - - if (win32Targets.length < 1 || windowsTargets.length > 0) { - return targets - } - - return [ - ...targets, - ...win32Targets.map((target) => { - return { - ...target, - platform: 'windows', - } - }), - ] -} - -export function dedupeAndGetLatestVersions(versions: CfnLspVersion[]): CfnLspVersion[] { - const grouped: Record = {} - - // Group by normalized version - for (const version of versions) { - const normalizedV = getMajorMinorPatchVersion(version.serverVersion) - if (!grouped[normalizedV]) { - grouped[normalizedV] = [] - } - grouped[normalizedV].push(version) - } - - const groupedAndSorted: Record = Object.fromEntries( - Object.entries(grouped).sort(([v1], [v2]) => { - return compareVersionsDesc(v1, v2) - }) - ) - - // Sort each group by version descending and pick the first (latest) - return Object.values(groupedAndSorted).map((group) => { - group.sort((a, b) => compareVersionsDesc(a.serverVersion, b.serverVersion)) - const latest = group[0] - latest.serverVersion = `${latest.serverVersion.replace('v', '')}` - - return latest // take the highest version - }) -} - -function compareVersionsDesc(v1: string, v2: string) { - const a = convertVersionToNumbers(v1) - const b = convertVersionToNumbers(v2) - - for (let i = 0; i < Math.max(a.length, b.length); i++) { - const partA = a[i] || 0 - const partB = b[i] || 0 - - if (partA > partB) { - return -1 - } - if (partA < partB) { - return 1 - } - } - return 0 -} - -function removeWordsFromVersion(version: string): string { - return version.replaceAll('-beta', '').replaceAll('-alpha', '').replaceAll('-prod', '').replaceAll('v', '') -} - -function convertVersionToNumbers(version: string): number[] { - return removeWordsFromVersion(version).replaceAll('-', '.').split('.').map(Number) -} - -function getMajorMinorPatchVersion(version: string): string { - return removeWordsFromVersion(version).split('-')[0] -} - -export function extractPlatformAndArch(filename: string): { platform: string; arch: string; nodejs?: string } { - const match = filename.match(/^cloudformation-languageserver-(.*)-(.*)-(x64|arm64)(?:-node(\d+))?\.zip$/) - if (match === null) { - throw new Error(`Could not extract platform from ${filename}`) - } - - const platform = match[2] - const arch = match[3] - const nodejs = match[4] - - if (!platform || !arch) { - throw new Error(`Unknown arch and platform ${arch} ${platform}`) - } - - return { arch, platform, nodejs } -} - export function useOldLinuxVersion(): boolean { if (process.platform !== 'linux') { return false diff --git a/packages/core/src/shared/lsp/manifestResolver.ts b/packages/core/src/shared/lsp/manifestResolver.ts index 6dd0a793178..3e63274f7a7 100644 --- a/packages/core/src/shared/lsp/manifestResolver.ts +++ b/packages/core/src/shared/lsp/manifestResolver.ts @@ -103,7 +103,7 @@ export class ManifestResolver { return manifest } - private parseManifest(content: string): Manifest { + protected parseManifest(content: string): Manifest { try { return JSON.parse(content) as Manifest } catch (error) { diff --git a/packages/core/src/test/awsService/cloudformation/lsp-server/cfnManifest.test.ts b/packages/core/src/test/awsService/cloudformation/lsp-server/cfnManifest.test.ts new file mode 100644 index 00000000000..6c5bf32ce34 --- /dev/null +++ b/packages/core/src/test/awsService/cloudformation/lsp-server/cfnManifest.test.ts @@ -0,0 +1,88 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { parseCfnManifest } from '../../../../awsService/cloudformation/lsp-server/cfnManifest' + +describe('parseCfnManifest', () => { + it('prefers version marked as latest', () => { + const manifest = parseCfnManifest( + JSON.stringify({ + manifestSchemaVersion: '1.0', + isManifestDeprecated: false, + prod: [ + { serverVersion: '1.4.0', latest: true, isDelisted: false, targets: [] }, + { serverVersion: '1.2.0', latest: false, isDelisted: false, targets: [] }, + ], + }), + 'prod' + ) + + assert.strictEqual(manifest.versions.length, 1) + assert.strictEqual(manifest.versions[0].serverVersion, '1.4.0') + }) + + it('falls back to all versions when no latest flag', () => { + const manifest = parseCfnManifest( + JSON.stringify({ + manifestSchemaVersion: '1.0', + isManifestDeprecated: false, + prod: [ + { serverVersion: '1.4.0', isDelisted: false, targets: [] }, + { serverVersion: '1.2.0', isDelisted: false, targets: [] }, + ], + }), + 'prod' + ) + + assert.strictEqual(manifest.versions.length, 2) + }) + + it('falls back to all versions when latest is delisted', () => { + const manifest = parseCfnManifest( + JSON.stringify({ + manifestSchemaVersion: '1.0', + isManifestDeprecated: false, + prod: [ + { serverVersion: '1.4.0', latest: true, isDelisted: true, targets: [] }, + { serverVersion: '1.2.0', latest: false, isDelisted: false, targets: [] }, + ], + }), + 'prod' + ) + + assert.strictEqual(manifest.versions.length, 2) + assert.strictEqual(manifest.versions[0].serverVersion, '1.4.0') + assert.strictEqual(manifest.versions[1].serverVersion, '1.2.0') + }) + + it('reads correct environment array', () => { + const manifest = parseCfnManifest( + JSON.stringify({ + manifestSchemaVersion: '1.0', + isManifestDeprecated: false, + prod: [{ serverVersion: '1.4.0', latest: true, isDelisted: false, targets: [] }], + beta: [{ serverVersion: '1.4.0-beta', latest: true, isDelisted: false, targets: [] }], + }), + 'beta' + ) + + assert.strictEqual(manifest.versions.length, 1) + assert.strictEqual(manifest.versions[0].serverVersion, '1.4.0-beta') + }) + + it('preserves isManifestDeprecated', () => { + const manifest = parseCfnManifest( + JSON.stringify({ + manifestSchemaVersion: '1.0', + isManifestDeprecated: true, + prod: [{ serverVersion: '1.4.0', latest: true, isDelisted: false, targets: [] }], + }), + 'prod' + ) + + assert.strictEqual(manifest.isManifestDeprecated, true) + }) +}) diff --git a/packages/core/src/test/awsService/cloudformation/lsp-server/utils.test.ts b/packages/core/src/test/awsService/cloudformation/lsp-server/utils.test.ts index 7ebfeb282bd..d140223da49 100644 --- a/packages/core/src/test/awsService/cloudformation/lsp-server/utils.test.ts +++ b/packages/core/src/test/awsService/cloudformation/lsp-server/utils.test.ts @@ -6,130 +6,11 @@ import assert from 'assert' import sinon from 'sinon' import { - addWindows, - dedupeAndGetLatestVersions, - extractPlatformAndArch, useOldLinuxVersion, mapLegacyLinux, - CfnTarget, CfnLspVersion, } from '../../../../awsService/cloudformation/lsp-server/utils' import { CLibCheck } from '../../../../awsService/cloudformation/lsp-server/CLibCheck' -import { LspVersion } from '../../../../shared/lsp/types' - -describe('addWindows', () => { - it('adds windows target when win32 exists and windows does not', () => { - const targets: CfnTarget[] = [ - { platform: 'darwin', arch: 'arm64', contents: [] }, - { platform: 'linux', arch: 'x64', contents: [] }, - { platform: 'win32', arch: 'x64', contents: [] }, - ] - - const result = addWindows(targets) - - assert.strictEqual(result.length, 4) - assert.ok(result.some((t) => t.platform === 'windows' && t.arch === 'x64')) - }) - - it('does not add windows target when windows already exists', () => { - const targets: CfnTarget[] = [ - { platform: 'darwin', arch: 'arm64', contents: [] }, - { platform: 'win32', arch: 'x64', contents: [] }, - { platform: 'windows', arch: 'x64', contents: [] }, - ] - - const result = addWindows(targets) - - assert.strictEqual(result.length, 3) - }) - - it('does not add windows target when no win32 exists', () => { - const targets: CfnTarget[] = [ - { platform: 'darwin', arch: 'arm64', contents: [] }, - { platform: 'linux', arch: 'x64', contents: [] }, - ] - - const result = addWindows(targets) - - assert.strictEqual(result.length, 2) - }) - - it('adds windows for multiple win32 architectures', () => { - const targets: CfnTarget[] = [ - { platform: 'win32', arch: 'x64', contents: [] }, - { platform: 'win32', arch: 'arm64', contents: [] }, - ] - - const result = addWindows(targets) - - assert.strictEqual(result.length, 4) - assert.strictEqual(result.filter((t) => t.platform === 'windows' && t.arch === 'x64').length, 1) - assert.strictEqual(result.filter((t) => t.platform === 'windows' && t.arch === 'arm64').length, 1) - }) -}) - -describe('extractPlatformAndArch', () => { - it('extracts platform, arch, and nodejs from standard filename', () => { - const result = extractPlatformAndArch('cloudformation-languageserver-1.2.0-beta-darwin-arm64-node22.zip') - - assert.strictEqual(result.platform, 'darwin') - assert.strictEqual(result.arch, 'arm64') - assert.strictEqual(result.nodejs, '22') - }) - - it('extracts linux platform with x64 arch', () => { - const result = extractPlatformAndArch('cloudformation-languageserver-1.2.0-beta-linux-x64-node22.zip') - - assert.strictEqual(result.platform, 'linux') - assert.strictEqual(result.arch, 'x64') - assert.strictEqual(result.nodejs, '22') - }) - - it('extracts linuxglib2.28 platform', () => { - const result = extractPlatformAndArch('cloudformation-languageserver-1.2.0-beta-linuxglib2.28-arm64-node18.zip') - - assert.strictEqual(result.platform, 'linuxglib2.28') - assert.strictEqual(result.arch, 'arm64') - assert.strictEqual(result.nodejs, '18') - }) - - it('extracts win32 platform', () => { - const result = extractPlatformAndArch('cloudformation-languageserver-1.2.0-beta-win32-x64-node22.zip') - - assert.strictEqual(result.platform, 'win32') - assert.strictEqual(result.arch, 'x64') - assert.strictEqual(result.nodejs, '22') - }) - - it('handles filename without node version', () => { - const result = extractPlatformAndArch('cloudformation-languageserver-1.1.0-darwin-arm64.zip') - - assert.strictEqual(result.platform, 'darwin') - assert.strictEqual(result.arch, 'arm64') - assert.strictEqual(result.nodejs, undefined) - }) - - it('handles alpha version with timestamp', () => { - const result = extractPlatformAndArch( - 'cloudformation-languageserver-1.2.0-202512020323-alpha-darwin-arm64-node22.zip' - ) - - assert.strictEqual(result.platform, 'darwin') - assert.strictEqual(result.arch, 'arm64') - assert.strictEqual(result.nodejs, '22') - }) - - it('throws error for invalid filename', () => { - assert.throws(() => extractPlatformAndArch('invalid-file.zip'), /Could not extract platform/) - }) - - it('throws error for unsupported architecture', () => { - assert.throws( - () => extractPlatformAndArch('cloudformation-languageserver-1.0.0-darwin-arm32-node22.zip'), - /Could not extract platform/ - ) - }) -}) describe('useOldLinuxVersion', () => { let sandbox: sinon.SinonSandbox @@ -190,41 +71,6 @@ describe('useOldLinuxVersion', () => { }) }) -describe('dedupeAndGetLatestVersions', () => { - for (const prefix of ['v', '']) { - it(`handles versions with timestamp: ${prefix}`, () => { - const result = dedupeAndGetLatestVersions( - generateLspVersion(['0.0.1-2020', '0.0.2-2024', '0.0.3-2026', '0.0.2-2025', '0.0.3-2030'], prefix) - ) - - assert.strictEqual(result.length, 3) - assert.strictEqual(result[0].serverVersion, '0.0.3-2030') - assert.strictEqual(result[1].serverVersion, '0.0.2-2025') - assert.strictEqual(result[2].serverVersion, '0.0.1-2020') - }) - - it('handles versions with timestamp and environment', () => { - const result = dedupeAndGetLatestVersions( - generateLspVersion( - ['0.0.1-2020-alpha', '0.0.2-2024-beta', '0.0.3-2026-alpha', '0.0.2-2025-prod', '0.0.3-2030-beta'], - prefix - ) - ) - - assert.strictEqual(result.length, 3) - assert.strictEqual(result[0].serverVersion, '0.0.3-2030-beta') - assert.strictEqual(result[1].serverVersion, '0.0.2-2025-prod') - assert.strictEqual(result[2].serverVersion, '0.0.1-2020-alpha') - }) - } - - function generateLspVersion(versions: string[], prefix: string = ''): LspVersion[] { - return versions.map((version) => { - return { serverVersion: `${prefix}${version}`, targets: [], isDelisted: false } - }) - } -}) - describe('mapLegacyLinux', () => { const darwinContent = { filename: 'darwin.zip', url: 'https://example.com/darwin.zip', hashes: ['abc'], bytes: 100 } const linuxContent = { filename: 'linux.zip', url: 'https://example.com/linux.zip', hashes: ['def'], bytes: 200 }