Skip to content

Commit a321cba

Browse files
authored
Refactor CLI version selection (#593)
1 parent 306af1c commit a321cba

File tree

8 files changed

+227
-173
lines changed

8 files changed

+227
-173
lines changed

extension/src/dependency-installer/installers/flow-cli-installer.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Installer, InstallerConstructor, InstallerContext } from '../installer'
66
import * as semver from 'semver'
77
import fetch from 'node-fetch'
88
import { HomebrewInstaller } from './homebrew-installer'
9+
import { KNOWN_FLOW_COMMANDS } from '../../flow-cli/cli-versions-provider'
910

1011
// Command to check flow-cli
1112
const COMPATIBLE_FLOW_CLI_VERSIONS = '>=1.6.0'
@@ -97,10 +98,9 @@ export class InstallFlowCLI extends Installer {
9798
}
9899
}
99100

100-
async checkVersion (vsn?: semver.SemVer): Promise<boolean> {
101+
async checkVersion (version: semver.SemVer): Promise<boolean> {
101102
// Get user's version informaton
102103
this.#context.cliProvider.refresh()
103-
const version = vsn ?? await this.#context.cliProvider.getAvailableBinaries().then(x => x.find(y => y.name === 'flow')?.version)
104104
if (version == null) return false
105105

106106
if (!semver.satisfies(version, COMPATIBLE_FLOW_CLI_VERSIONS, {
@@ -128,7 +128,11 @@ export class InstallFlowCLI extends Installer {
128128
async verifyInstall (): Promise<boolean> {
129129
// Check if flow version is valid to verify install
130130
this.#context.cliProvider.refresh()
131-
const version = await this.#context.cliProvider.getAvailableBinaries().then(x => x.find(y => y.name === 'flow')?.version)
131+
const installedVersions = await this.#context.cliProvider.getBinaryVersions().catch((e) => {
132+
void window.showErrorMessage(`Failed to check CLI version: ${String(e.message)}`)
133+
return []
134+
})
135+
const version = installedVersions.find(y => y.command === KNOWN_FLOW_COMMANDS.DEFAULT)?.version
132136
if (version == null) return false
133137

134138
// Check flow-cli version number
Lines changed: 35 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,37 @@
11
import { BehaviorSubject, Observable, distinctUntilChanged, pairwise, startWith } from 'rxjs'
2-
import { execDefault } from '../utils/shell/exec'
32
import { StateCache } from '../utils/state-cache'
4-
import * as semver from 'semver'
53
import * as vscode from 'vscode'
64
import { Settings } from '../settings/settings'
75
import { isEqual } from 'lodash'
8-
9-
const CHECK_FLOW_CLI_CMD = (flowCommand: string): string => `${flowCommand} version --output=json`
10-
const CHECK_FLOW_CLI_CMD_NO_JSON = (flowCommand: string): string => `${flowCommand} version`
11-
12-
const KNOWN_BINS = ['flow', 'flow-c1']
13-
14-
const CADENCE_V1_CLI_REGEX = /-cadence-v1.0.0/g
15-
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
16-
17-
export interface CliBinary {
18-
name: string
19-
version: semver.SemVer
20-
}
21-
22-
interface FlowVersionOutput {
23-
version: string
24-
}
25-
26-
interface AvailableBinariesCache {
27-
[key: string]: StateCache<CliBinary | null>
28-
}
6+
import { CliBinary, CliVersionsProvider, KNOWN_FLOW_COMMANDS } from './cli-versions-provider'
297

308
export class CliProvider {
319
#selectedBinaryName: BehaviorSubject<string>
3210
#currentBinary$: StateCache<CliBinary | null>
33-
#availableBinaries: AvailableBinariesCache = {}
34-
#availableBinaries$: StateCache<CliBinary[]>
11+
#cliVersionsProvider: CliVersionsProvider
3512
#settings: Settings
3613

3714
constructor (settings: Settings) {
15+
const initialBinaryPath = settings.getSettings().flowCommand
16+
3817
this.#settings = settings
18+
this.#cliVersionsProvider = new CliVersionsProvider([initialBinaryPath])
19+
this.#selectedBinaryName = new BehaviorSubject<string>(initialBinaryPath)
20+
this.#currentBinary$ = new StateCache(async () => {
21+
const name: string = this.#selectedBinaryName.getValue()
22+
const versionCache = this.#cliVersionsProvider.get(name)
23+
if (versionCache == null) return null
24+
return await versionCache.getValue()
25+
})
3926

40-
this.#selectedBinaryName = new BehaviorSubject<string>(settings.getSettings().flowCommand)
27+
// Bind the selected binary to the settings
4128
this.#settings.watch$(config => config.flowCommand).subscribe((flowCommand) => {
4229
this.#selectedBinaryName.next(flowCommand)
4330
})
4431

45-
this.#availableBinaries = KNOWN_BINS.reduce<AvailableBinariesCache>((acc, bin) => {
46-
acc[bin] = new StateCache(async () => await this.#fetchBinaryInformation(bin))
47-
acc[bin].subscribe(() => {
48-
this.#availableBinaries$.invalidate()
49-
})
50-
return acc
51-
}, {})
52-
53-
this.#availableBinaries$ = new StateCache(async () => {
54-
return await this.getAvailableBinaries()
55-
})
56-
57-
this.#currentBinary$ = new StateCache(async () => {
58-
const name: string = this.#selectedBinaryName.getValue()
59-
return await this.#availableBinaries[name].getValue()
60-
})
61-
6232
// Display warning to user if binary doesn't exist (only if not using the default binary)
63-
this.#currentBinary$.subscribe((binary) => {
64-
if (binary === null && this.#selectedBinaryName.getValue() !== 'flow') {
33+
this.currentBinary$.subscribe((binary) => {
34+
if (binary === null && this.#selectedBinaryName.getValue() !== KNOWN_FLOW_COMMANDS.DEFAULT) {
6535
void vscode.window.showErrorMessage(`The configured Flow CLI binary "${this.#selectedBinaryName.getValue()}" does not exist. Please check your settings.`)
6636
}
6737
})
@@ -72,119 +42,43 @@ export class CliProvider {
7242
#watchForBinaryChanges (): void {
7343
// Subscribe to changes in the selected binary to update the caches
7444
this.#selectedBinaryName.pipe(distinctUntilChanged(), startWith(null), pairwise()).subscribe(([prev, curr]) => {
75-
// Swap out the cache for the selected binary
76-
if (prev != null && !KNOWN_BINS.includes(prev)) {
77-
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
78-
delete this.#availableBinaries[prev]
79-
}
80-
if (curr != null && !KNOWN_BINS.includes(curr)) {
81-
this.#availableBinaries[curr] = new StateCache(async () => await this.#fetchBinaryInformation(curr))
82-
this.#availableBinaries[curr].subscribe(() => {
83-
this.#availableBinaries$.invalidate()
84-
})
85-
}
45+
// Remove the previous binary from the cache
46+
if (prev != null) this.#cliVersionsProvider.remove(prev)
47+
48+
// Add the current binary to the cache
49+
if (curr != null) this.#cliVersionsProvider.add(curr)
8650

8751
// Invalidate the current binary cache
8852
this.#currentBinary$.invalidate()
89-
90-
// Invalidate the available binaries cache
91-
this.#availableBinaries$.invalidate()
9253
})
9354
}
9455

95-
// Fetches the binary information for the given binary
96-
async #fetchBinaryInformation (bin: string): Promise<CliBinary | null> {
97-
try {
98-
// Get user's version informaton
99-
const buffer: string = (await execDefault(CHECK_FLOW_CLI_CMD(
100-
bin
101-
))).stdout
102-
103-
// Format version string from output
104-
const versionInfo: FlowVersionOutput = JSON.parse(buffer)
105-
106-
// Ensure user has a compatible version number installed
107-
const version: semver.SemVer | null = semver.parse(versionInfo.version)
108-
if (version === null) return null
109-
110-
return { name: bin, version }
111-
} catch {
112-
// Fallback to old method if JSON is not supported/fails
113-
return await this.#fetchBinaryInformationOld(bin)
114-
}
115-
}
116-
117-
// Old version of fetchBinaryInformation (before JSON was supported)
118-
// Used as fallback for old CLI versions
119-
async #fetchBinaryInformationOld (bin: string): Promise<CliBinary | null> {
120-
try {
121-
// Get user's version informaton
122-
const output = (await execDefault(CHECK_FLOW_CLI_CMD_NO_JSON(
123-
bin
124-
)))
125-
126-
let versionStr: string | null = parseFlowCliVersion(output.stdout)
127-
if (versionStr === null) {
128-
// Try to fallback to stderr as patch for bugged version
129-
versionStr = parseFlowCliVersion(output.stderr)
130-
}
131-
132-
versionStr = versionStr != null ? semver.clean(versionStr) : null
133-
if (versionStr === null) return null
134-
135-
// Ensure user has a compatible version number installed
136-
const version: semver.SemVer | null = semver.parse(versionStr)
137-
if (version === null) return null
138-
139-
return { name: bin, version }
140-
} catch {
141-
return null
142-
}
143-
}
144-
145-
refresh (): void {
146-
for (const bin in this.#availableBinaries) {
147-
this.#availableBinaries[bin].invalidate()
148-
}
149-
this.#currentBinary$.invalidate()
150-
}
151-
152-
get availableBinaries$ (): Observable<CliBinary[]> {
153-
return new Observable((subscriber) => {
154-
this.#availableBinaries$.subscribe((binaries) => {
155-
subscriber.next(binaries)
156-
})
157-
}).pipe(distinctUntilChanged(isEqual))
56+
async getCurrentBinary (): Promise<CliBinary | null> {
57+
return await this.#currentBinary$.getValue()
15858
}
15959

160-
async getAvailableBinaries (): Promise<CliBinary[]> {
161-
const bins: CliBinary[] = []
162-
for (const name in this.#availableBinaries) {
163-
const binary = await this.#availableBinaries[name].getValue().catch(() => null)
164-
if (binary !== null) {
165-
bins.push(binary)
166-
}
60+
async setCurrentBinary (name: string): Promise<void> {
61+
if (vscode.workspace.workspaceFolders == null) {
62+
await this.#settings.updateSettings({ flowCommand: name }, vscode.ConfigurationTarget.Global)
63+
} else {
64+
await this.#settings.updateSettings({ flowCommand: name })
16765
}
168-
return bins
16966
}
17067

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

175-
async getCurrentBinary (): Promise<CliBinary | null> {
176-
return await this.#currentBinary$.getValue()
72+
async getBinaryVersions (): Promise<CliBinary[]> {
73+
return await this.#cliVersionsProvider.getVersions()
17774
}
17875

179-
async setCurrentBinary (name: string): Promise<void> {
180-
await this.#settings.updateSettings({ flowCommand: name })
76+
get binaryVersions$ (): Observable<CliBinary[]> {
77+
return this.#cliVersionsProvider.versions$.pipe(distinctUntilChanged(isEqual))
18178
}
182-
}
183-
184-
export function isCadenceV1Cli (version: semver.SemVer): boolean {
185-
return CADENCE_V1_CLI_REGEX.test(version.raw)
186-
}
18779

188-
export function parseFlowCliVersion (buffer: Buffer | string): string | null {
189-
return buffer.toString().match(LEGACY_VERSION_REGEXP)?.[1] ?? null
80+
// Refresh all cached binary versions
81+
refresh (): void {
82+
this.#cliVersionsProvider.refresh()
83+
}
19084
}

extension/src/flow-cli/cli-selection-provider.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import * as vscode from 'vscode'
12
import { zip } from 'rxjs'
2-
import { CliBinary, CliProvider } from './cli-provider'
3+
import { CliProvider } from './cli-provider'
34
import { SemVer } from 'semver'
4-
import * as vscode from 'vscode'
5+
import { CliBinary } from './cli-versions-provider'
56

6-
const CHANGE_CADENCE_VERSION = 'cadence.changeCadenceVersion'
7+
const CHANGE_CLI_BINARY = 'cadence.changeFlowCliBinary'
78
const CADENCE_V1_CLI_REGEX = /-cadence-v1.0.0/g
89
// label with icon
910
const GET_BINARY_LABEL = (version: SemVer): string => `Flow CLI v${version.format()}`
@@ -19,13 +20,13 @@ export class CliSelectionProvider {
1920
this.#cliProvider = cliProvider
2021

2122
// Register the command to toggle the version
22-
this.#disposables.push(vscode.commands.registerCommand(CHANGE_CADENCE_VERSION, async () => {
23+
this.#disposables.push(vscode.commands.registerCommand(CHANGE_CLI_BINARY, async () => {
2324
this.#cliProvider.refresh()
2425
await this.#toggleSelector(true)
2526
}))
2627

2728
// Register UI components
28-
zip(this.#cliProvider.currentBinary$, this.#cliProvider.availableBinaries$).subscribe(() => {
29+
zip(this.#cliProvider.currentBinary$, this.#cliProvider.binaryVersions$).subscribe(() => {
2930
void this.#refreshSelector()
3031
})
3132
this.#cliProvider.currentBinary$.subscribe((binary) => {
@@ -37,7 +38,7 @@ export class CliSelectionProvider {
3738

3839
#createStatusBarItem (version: SemVer | null): vscode.StatusBarItem {
3940
const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1)
40-
statusBarItem.command = CHANGE_CADENCE_VERSION
41+
statusBarItem.command = CHANGE_CLI_BINARY
4142
statusBarItem.color = new vscode.ThemeColor('statusBar.foreground')
4243
statusBarItem.tooltip = 'Click to change the Flow CLI version'
4344

@@ -74,23 +75,31 @@ export class CliSelectionProvider {
7475
}
7576
})
7677
} else if (selected instanceof AvailableBinaryItem) {
77-
void this.#cliProvider.setCurrentBinary(selected.path)
78+
void this.#cliProvider.setCurrentBinary(selected.command)
7879
}
7980
}))
8081

82+
this.#disposables.push(versionSelector.onDidHide(() => {
83+
void this.#toggleSelector(false)
84+
}))
85+
8186
// Update available versions
8287
const items: Array<AvailableBinaryItem | CustomBinaryItem> = availableBinaries.map(binary => new AvailableBinaryItem(binary))
8388
items.push(new CustomBinaryItem())
84-
versionSelector.items = items
8589

86-
// Select the current binary
87-
if (currentBinary !== null) {
88-
const currentBinaryItem = versionSelector.items.find(item => item instanceof AvailableBinaryItem && item.path === currentBinary.name)
89-
if (currentBinaryItem != null) {
90-
versionSelector.selectedItems = [currentBinaryItem]
91-
}
90+
// Hoist the current binary to the top of the list
91+
const currentBinaryIndex = items.findIndex(item =>
92+
item instanceof AvailableBinaryItem &&
93+
currentBinary != null &&
94+
item.command === currentBinary.command
95+
)
96+
if (currentBinaryIndex !== -1) {
97+
const currentBinaryItem = items[currentBinaryIndex]
98+
items.splice(currentBinaryIndex, 1)
99+
items.unshift(currentBinaryItem)
92100
}
93101

102+
versionSelector.items = items
94103
return versionSelector
95104
}
96105

@@ -103,7 +112,7 @@ export class CliSelectionProvider {
103112
if (this.#showSelector) {
104113
this.#versionSelector?.dispose()
105114
const currentBinary = await this.#cliProvider.getCurrentBinary()
106-
const availableBinaries = await this.#cliProvider.getAvailableBinaries()
115+
const availableBinaries = await this.#cliProvider.getBinaryVersions()
107116
this.#versionSelector = this.#createVersionSelector(currentBinary, availableBinaries)
108117
this.#disposables.push(this.#versionSelector)
109118
this.#versionSelector.show()
@@ -134,11 +143,11 @@ class AvailableBinaryItem implements vscode.QuickPickItem {
134143
}
135144

136145
get description (): string {
137-
return `(${this.#binary.name})`
146+
return `(${this.#binary.command})`
138147
}
139148

140-
get path (): string {
141-
return this.#binary.name
149+
get command (): string {
150+
return this.#binary.command
142151
}
143152
}
144153

0 commit comments

Comments
 (0)