Skip to content

Commit

Permalink
Refactor CLI version selection
Browse files Browse the repository at this point in the history
  • Loading branch information
jribbink committed Apr 15, 2024
1 parent 306af1c commit 4b266f8
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 154 deletions.
141 changes: 141 additions & 0 deletions extension/src/flow-cli/binary-versions-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import * as semver from 'semver'
import { StateCache } from '../utils/state-cache'
import { execDefault } from '../utils/shell/exec'
import { Observable, distinctUntilChanged } from 'rxjs'
import { isEqual } from 'lodash'

const CHECK_FLOW_CLI_CMD = (flowCommand: string): string => `${flowCommand} version --output=json`
const CHECK_FLOW_CLI_CMD_NO_JSON = (flowCommand: string): string => `${flowCommand} version`

const KNOWN_BINS = ['flow', 'flow-c1']

const LEGACY_VERSION_REGEXP = /Version:\s*(v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)(\s|$)/m

export interface CliBinary {
path: string
version: semver.SemVer
}

interface FlowVersionOutput {
version: string
}

export class BinaryVersionsProvider {
#rootCache: StateCache<CliBinary[]>
#caches: { [key: string]: StateCache<CliBinary | null> } = {}

constructor (seedBinaries: string[] = []) {
// Seed the caches with the known binaries
KNOWN_BINS.forEach((bin) => {
this.add(bin)
})

// Seed the caches with any additional binaries
seedBinaries.forEach((bin) => {
this.add(bin)
})

// Create the root cache. This cache will hold all the binary information
// and is a combination of all the individual caches for each binary
this.#rootCache = new StateCache(async () => {
const binaries = await Promise.all(
Object.keys(this.#caches).map(async (bin) => {
return await this.#caches[bin].getValue().catch(() => null)
})
)

// Filter out missing binaries
return binaries.filter((bin) => bin != null) as CliBinary[]
})
}

add (path: string): void {
if (this.#caches[path] != null) return
this.#caches[path] = new StateCache(async () => await this.#fetchBinaryInformation(path))
this.#caches[path].subscribe(() => {
this.#rootCache?.invalidate()
})
this.#rootCache?.invalidate()
}

remove (path: string): void {
// Known binaries cannot be removed
if (this.#caches[path] == null || KNOWN_BINS.includes(path)) return
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete this.#caches[path]
this.#rootCache?.invalidate()
}

get (name: string): StateCache<CliBinary | null> | null {
return this.#caches[name] ?? null
}

// Fetches the binary information for the given binary
async #fetchBinaryInformation (bin: string): Promise<CliBinary | null> {
try {
// Get user's version informaton
const buffer: string = (await execDefault(CHECK_FLOW_CLI_CMD(
bin
))).stdout

// Format version string from output
const versionInfo: FlowVersionOutput = JSON.parse(buffer)

// Ensure user has a compatible version number installed
const version: semver.SemVer | null = semver.parse(versionInfo.version)
if (version === null) return null

return { path: bin, version }
} catch {
// Fallback to old method if JSON is not supported/fails
return await this.#fetchBinaryInformationOld(bin)
}
}

// Old version of fetchBinaryInformation (before JSON was supported)
// Used as fallback for old CLI versions
async #fetchBinaryInformationOld (bin: string): Promise<CliBinary | null> {
try {
// Get user's version informaton
const output = (await execDefault(CHECK_FLOW_CLI_CMD_NO_JSON(
bin
)))

let versionStr: string | null = parseFlowCliVersion(output.stdout)
if (versionStr === null) {
// Try to fallback to stderr as patch for bugged version
versionStr = parseFlowCliVersion(output.stderr)
}

versionStr = versionStr != null ? semver.clean(versionStr) : null
if (versionStr === null) return null

// Ensure user has a compatible version number installed
const version: semver.SemVer | null = semver.parse(versionStr)
if (version === null) return null

return { path: bin, version }
} catch {
return null
}
}

refresh (): void {
Object.keys(this.#caches).forEach((bin) => {
this.#caches[bin].invalidate()
})
this.#rootCache.invalidate()
}

async getVersions (): Promise<CliBinary[]> {
return await this.#rootCache.getValue()
}

get versions$ (): Observable<CliBinary[]> {
return this.#rootCache.pipe(distinctUntilChanged(isEqual))
}
}

export function parseFlowCliVersion (buffer: Buffer | string): string | null {
return buffer.toString().match(LEGACY_VERSION_REGEXP)?.[1] ?? null
}
172 changes: 31 additions & 141 deletions extension/src/flow-cli/cli-provider.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,36 @@
import { BehaviorSubject, Observable, distinctUntilChanged, pairwise, startWith } from 'rxjs'
import { execDefault } from '../utils/shell/exec'
import { StateCache } from '../utils/state-cache'
import * as semver from 'semver'
import * as vscode from 'vscode'
import { Settings } from '../settings/settings'
import { isEqual } from 'lodash'

const CHECK_FLOW_CLI_CMD = (flowCommand: string): string => `${flowCommand} version --output=json`
const CHECK_FLOW_CLI_CMD_NO_JSON = (flowCommand: string): string => `${flowCommand} version`

const KNOWN_BINS = ['flow', 'flow-c1']

const CADENCE_V1_CLI_REGEX = /-cadence-v1.0.0/g
const LEGACY_VERSION_REGEXP = /Version:\s*(v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)(\s|$)/m

export interface CliBinary {
name: string
version: semver.SemVer
}

interface FlowVersionOutput {
version: string
}

interface AvailableBinariesCache {
[key: string]: StateCache<CliBinary | null>
}
import { CliBinary, BinaryVersionsProvider } from './binary-versions-provider'

export class CliProvider {
#selectedBinaryName: BehaviorSubject<string>
#currentBinary$: StateCache<CliBinary | null>
#availableBinaries: AvailableBinariesCache = {}
#availableBinaries$: StateCache<CliBinary[]>
#binaryVersions: BinaryVersionsProvider
#settings: Settings

constructor (settings: Settings) {
const initialBinaryPath = settings.getSettings().flowCommand

this.#settings = settings
this.#binaryVersions = new BinaryVersionsProvider([initialBinaryPath])
this.#selectedBinaryName = new BehaviorSubject<string>(initialBinaryPath)
this.#currentBinary$ = new StateCache(async () => {
const name: string = this.#selectedBinaryName.getValue()
const versionCache = this.#binaryVersions.get(name)
if (versionCache == null) return null
return await versionCache.getValue()
})

this.#selectedBinaryName = new BehaviorSubject<string>(settings.getSettings().flowCommand)
// Bind the selected binary to the settings
this.#settings.watch$(config => config.flowCommand).subscribe((flowCommand) => {
this.#selectedBinaryName.next(flowCommand)
})

this.#availableBinaries = KNOWN_BINS.reduce<AvailableBinariesCache>((acc, bin) => {
acc[bin] = new StateCache(async () => await this.#fetchBinaryInformation(bin))
acc[bin].subscribe(() => {
this.#availableBinaries$.invalidate()
})
return acc
}, {})

this.#availableBinaries$ = new StateCache(async () => {
return await this.getAvailableBinaries()
})

this.#currentBinary$ = new StateCache(async () => {
const name: string = this.#selectedBinaryName.getValue()
return await this.#availableBinaries[name].getValue()
})

// Display warning to user if binary doesn't exist (only if not using the default binary)
this.#currentBinary$.subscribe((binary) => {
this.currentBinary$.subscribe((binary) => {
if (binary === null && this.#selectedBinaryName.getValue() !== 'flow') {
void vscode.window.showErrorMessage(`The configured Flow CLI binary "${this.#selectedBinaryName.getValue()}" does not exist. Please check your settings.`)
}
Expand All @@ -72,119 +42,39 @@ export class CliProvider {
#watchForBinaryChanges (): void {
// Subscribe to changes in the selected binary to update the caches
this.#selectedBinaryName.pipe(distinctUntilChanged(), startWith(null), pairwise()).subscribe(([prev, curr]) => {
// Swap out the cache for the selected binary
if (prev != null && !KNOWN_BINS.includes(prev)) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete this.#availableBinaries[prev]
}
if (curr != null && !KNOWN_BINS.includes(curr)) {
this.#availableBinaries[curr] = new StateCache(async () => await this.#fetchBinaryInformation(curr))
this.#availableBinaries[curr].subscribe(() => {
this.#availableBinaries$.invalidate()
})
}
// Remove the previous binary from the cache
if (prev != null) this.#binaryVersions.remove(prev)

// Add the current binary to the cache
if (curr != null) this.#binaryVersions.add(curr)

// Invalidate the current binary cache
this.#currentBinary$.invalidate()

// Invalidate the available binaries cache
this.#availableBinaries$.invalidate()
})
}

// Fetches the binary information for the given binary
async #fetchBinaryInformation (bin: string): Promise<CliBinary | null> {
try {
// Get user's version informaton
const buffer: string = (await execDefault(CHECK_FLOW_CLI_CMD(
bin
))).stdout

// Format version string from output
const versionInfo: FlowVersionOutput = JSON.parse(buffer)

// Ensure user has a compatible version number installed
const version: semver.SemVer | null = semver.parse(versionInfo.version)
if (version === null) return null

return { name: bin, version }
} catch {
// Fallback to old method if JSON is not supported/fails
return await this.#fetchBinaryInformationOld(bin)
}
}

// Old version of fetchBinaryInformation (before JSON was supported)
// Used as fallback for old CLI versions
async #fetchBinaryInformationOld (bin: string): Promise<CliBinary | null> {
try {
// Get user's version informaton
const output = (await execDefault(CHECK_FLOW_CLI_CMD_NO_JSON(
bin
)))

let versionStr: string | null = parseFlowCliVersion(output.stdout)
if (versionStr === null) {
// Try to fallback to stderr as patch for bugged version
versionStr = parseFlowCliVersion(output.stderr)
}

versionStr = versionStr != null ? semver.clean(versionStr) : null
if (versionStr === null) return null

// Ensure user has a compatible version number installed
const version: semver.SemVer | null = semver.parse(versionStr)
if (version === null) return null

return { name: bin, version }
} catch {
return null
}
}

refresh (): void {
for (const bin in this.#availableBinaries) {
this.#availableBinaries[bin].invalidate()
}
this.#currentBinary$.invalidate()
}

get availableBinaries$ (): Observable<CliBinary[]> {
return new Observable((subscriber) => {
this.#availableBinaries$.subscribe((binaries) => {
subscriber.next(binaries)
})
}).pipe(distinctUntilChanged(isEqual))
async getCurrentBinary (): Promise<CliBinary | null> {
return await this.#currentBinary$.getValue()
}

async getAvailableBinaries (): Promise<CliBinary[]> {
const bins: CliBinary[] = []
for (const name in this.#availableBinaries) {
const binary = await this.#availableBinaries[name].getValue().catch(() => null)
if (binary !== null) {
bins.push(binary)
}
}
return bins
async setCurrentBinary (name: string): Promise<void> {
await this.#settings.updateSettings({ flowCommand: name })
}

get currentBinary$ (): Observable<CliBinary | null> {
return this.#currentBinary$.pipe(distinctUntilChanged(isEqual))
}

async getCurrentBinary (): Promise<CliBinary | null> {
return await this.#currentBinary$.getValue()
async getBinaryVersions (): Promise<CliBinary[]> {
return await this.#binaryVersions.getVersions()
}

async setCurrentBinary (name: string): Promise<void> {
await this.#settings.updateSettings({ flowCommand: name })
get binaryVersions$ (): Observable<CliBinary[]> {
return this.#binaryVersions.versions$.pipe(distinctUntilChanged(isEqual))
}
}

export function isCadenceV1Cli (version: semver.SemVer): boolean {
return CADENCE_V1_CLI_REGEX.test(version.raw)
}

export function parseFlowCliVersion (buffer: Buffer | string): string | null {
return buffer.toString().match(LEGACY_VERSION_REGEXP)?.[1] ?? null
// Refresh all cached binary versions
refresh (): void {
this.#binaryVersions.refresh()
}
}
Loading

0 comments on commit 4b266f8

Please sign in to comment.