Skip to content

Commit

Permalink
stash
Browse files Browse the repository at this point in the history
  • Loading branch information
jribbink committed Mar 18, 2024
1 parent ebae385 commit f53c020
Show file tree
Hide file tree
Showing 9 changed files with 283 additions and 112 deletions.
2 changes: 2 additions & 0 deletions extension/src/dependency-installer/dependency-installer.ts
Expand Up @@ -21,6 +21,8 @@ export class DependencyInstaller {
languageServerApi,
cliProvider
}

// Register installers
this.#registerInstallers()

// Create state cache for missing dependencies
Expand Down
139 changes: 139 additions & 0 deletions extension/src/dependency-installer/installers/c1-flow-cli-installer.ts
@@ -0,0 +1,139 @@
/* Installer for Flow CLI */
import { window } from 'vscode'
import { execVscodeTerminal, tryExecPowerShell, tryExecUnixDefault } from '../../utils/shell/exec'
import { promptUserInfoMessage, promptUserErrorMessage } from '../../ui/prompts'
import { Installer, InstallerContext } from '../installer'
import * as semver from 'semver'
import fetch from 'node-fetch'
import { isCadenceV1Cli } from '../../flow-cli/cli-provider'

// Command to check flow-cli
const COMPATIBLE_FLOW_CLI_VERSIONS = '*'

// Shell install commands
const POWERSHELL_INSTALL_CMD = (githubToken?: string): string =>
`iex "& { $(irm 'https://raw.githubusercontent.com/onflow/flow-cli/feature/stable-cadence/install.ps1') } ${
githubToken != null ? `-GitHubToken ${githubToken} ` : ''
}"`
const BASH_INSTALL_FLOW_CLI = (githubToken?: string): string =>
`${
githubToken != null ? `GITHUB_TOKEN=${githubToken} ` : ''
}sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/feature/stable-cadence/install.sh)"`

const RELEASES_URL = (page: number) => `https://api.github.com/repos/onflow/flow-cli/releases?per_page=100&page=${page}`

export class InstallC1FlowCLI extends Installer {
#githubToken: string | undefined
#context: InstallerContext

constructor (context: InstallerContext) {
super('Cadence 1.0 Flow CLI', [])
this.#githubToken = process.env.GITHUB_TOKEN
this.#context = context
}

async install (): Promise<void> {
const isActive = this.#context.languageServerApi.isActive ?? false
if (isActive) await this.#context.languageServerApi.deactivate()
const OS_TYPE = process.platform

try {
switch (OS_TYPE) {
case 'win32':
await this.#install_windows()
break
default:
await this.#install_bash_cmd()
break
}
} catch {
void window.showErrorMessage('Failed to install Cadence 1.0 Flow CLI')
}
if (isActive) await this.#context.languageServerApi.activate()
}

async #install_windows (): Promise<void> {
// Retry if bad GH token
if (this.#githubToken != null && await tryExecPowerShell(POWERSHELL_INSTALL_CMD(this.#githubToken))) { return }
await execVscodeTerminal('Install Cadence 1.0 Flow CLI', POWERSHELL_INSTALL_CMD(this.#githubToken))
}

async #install_bash_cmd (): Promise<void> {
// Retry if bad GH token
if (this.#githubToken != null && await tryExecUnixDefault(BASH_INSTALL_FLOW_CLI(this.#githubToken))) { return }
await execVscodeTerminal('Install Cadence 1.0 Flow CLI', BASH_INSTALL_FLOW_CLI())
}

async findLatestVersion (currentVersion: semver.SemVer): Promise<void> {
// Recursive function to find cadence v1 release
async function findLatestRelease (page: number): Promise<semver.SemVer> {
return fetch(RELEASES_URL(page)).then(async (response: Response) => {
if (!response.ok) {
throw new Error(`Failed to fetch releases from flow-cli repo: ${response.statusText}`)
}
return await response.json()
}).then((releases: any[]) => {
if (releases.length === 0) {
throw new Error('No releases found')
}
const version = semver.parse(releases[0].tag_name)
if (version != null && isCadenceV1Cli(version)) return version
return findLatestRelease(page + 1)
})
}

const latest = await findLatestRelease(1)

// Check if latest version > current version
if (semver.compare(latest, currentVersion) === 1) {
promptUserInfoMessage(
'There is a new Cadence 1.0 Flow CLI version available: ' + latest.format(),
[{
label: 'Install latest Cadence 1.0 Flow CLI',
callback: async () => {
await this.runInstall()
await this.#context.refreshDependencies()
}
}]
)
}
}

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

if (!semver.satisfies(version, COMPATIBLE_FLOW_CLI_VERSIONS, {
includePrerelease: true
})) {
promptUserErrorMessage(
'Incompatible Cadence 1.0 Flow CLI version: ' + version.format(),
[{
label: 'Install latest Cadence 1.0 Flow CLI',
callback: async () => {
await this.runInstall()
await this.#context.refreshDependencies()
}
}]
)
return false
}

// Check for newer version
await this.findLatestVersion(version)

return true
}

async verifyInstall (): Promise<boolean> {
// Check if flow version is valid to verify install
this.#context.cliProvider.refresh()
const version = await this.#context.cliProvider.getAvailableBinaries().then(x => x.find(y => y.name === 'flow-c1')?.version)
if (version == null) return false

// Check flow-cli version number
return await this.checkVersion(version)
}
}
24 changes: 7 additions & 17 deletions extension/src/flow-cli/cli-provider.ts
@@ -1,12 +1,13 @@
import { BehaviorSubject, Observable, Subject, distinctUntilChanged, pairwise, startWith } from 'rxjs'
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 { Settings } from '../settings/settings'

const CHECK_FLOW_CLI_CMD = (flowCommand: string): string => `${flowCommand} version`
const KNOWN_BINS = ['flow', 'flow-c1']
const DEFAULT_BIN = 'flow'

const CADENCE_V1_CLI_REGEX = /-cadence-v1.0.0/g

export type CliBinary = {
name: string
Expand All @@ -28,7 +29,7 @@ export class CliProvider {
this.#settings = settings

this.#selectedBinaryName = new BehaviorSubject<string>(settings.getSettings().flowCommand)
this.#settings.watch$(config => config.flowCommand).subscribe((flowCommand) => {
this.#settings.settings$(config => config.flowCommand).subscribe((flowCommand) => {
this.#selectedBinaryName.next(flowCommand)
})

Expand Down Expand Up @@ -120,17 +121,6 @@ export class CliProvider {
return bins
}

/*async getMissingBinaries (): Promise<string[]> {
const bins: string[] = []
for (const bin in this.#availableBinaries) {
const version: semver.SemVer | null = await this.#availableBinaries[bin].getValue().catch(() => null)
if (version === null) {
bins.push(bin)
}
}
return bins
}*/

get currentBinary$ (): Observable<CliBinary | null> {
return this.#currentBinary$
}
Expand All @@ -139,13 +129,13 @@ export class CliProvider {
return this.#currentBinary$.getValue()
}

setSelectedBinary (name: string): void {
setCurrentBinary (name: string): void {
this.#settings.updateSettings({ flowCommand: name })
}
}

function isCadenceV1CLI (version: semver.SemVer): boolean {
return /-cadence-v1.0.0/g.test(version.raw)
export function isCadenceV1Cli (version: semver.SemVer): boolean {
return CADENCE_V1_CLI_REGEX.test(version.raw)
}

export function parseFlowCliVersion (buffer: Buffer | string): string {
Expand Down
75 changes: 32 additions & 43 deletions extension/src/json-schema-provider.ts
Expand Up @@ -2,52 +2,62 @@ import * as vscode from 'vscode'
import { readFile } from 'fs'
import { promisify } from 'util'
import { resolve } from 'path'
import { SemVer } from 'semver'
import fetch from 'node-fetch'
import { StateCache } from './utils/state-cache'
import { Subscription } from 'rxjs'
import { CliProvider } from './flow-cli/cli-provider'

const CADENCE_SCHEMA_URI = 'cadence-schema'
const GET_FLOW_SCHEMA_URL = (version: SemVer): string => `https://raw.githubusercontent.com/onflow/flow-cli/v${version.format()}/flowkit/schema.json`
const GET_FLOW_SCHEMA_URL = (version: string): string => `https://raw.githubusercontent.com/onflow/flow-cli/v${version}/flowkit/schema.json`

// This class provides the JSON schema for the flow.json file
// It is accessible via the URI scheme "cadence-schema:///flow.json"
export class JSONSchemaProvider implements vscode.FileSystemProvider, vscode.Disposable {
#contentProviderDisposable: vscode.Disposable | undefined
#flowVersionSubscription: Subscription
#extensionPath: string
#flowVersion: StateCache<SemVer | null>
#flowSchema: StateCache<string>
#showLocalError: boolean = false
#cliProvider: CliProvider
#schemaCache: { [version: string]: Promise<string> } = {}

constructor (
extensionPath: string,
flowVersion: StateCache<SemVer | null>,
cliProvider: CliProvider
) {
this.#extensionPath = extensionPath
this.#flowVersion = flowVersion
this.#flowSchema = new StateCache(async () => await this.#resolveFlowSchema())
this.#cliProvider = cliProvider

// Register the schema provider
this.#contentProviderDisposable = vscode.workspace.registerFileSystemProvider(
CADENCE_SCHEMA_URI,
this
)
}

// Invalidate the schema when the flow-cli version changes
this.#flowVersionSubscription = this.#flowVersion.subscribe(
() => this.#flowSchema.invalidate()
)
async #getFlowSchema (): Promise<string> {
const cliBinary = await this.#cliProvider.getCurrentBinary()
if (cliBinary == null) {
throw new Error('No flow-cli binary found')
}

const version = cliBinary.version.format()
if (this.#schemaCache[version] == null) {
this.#schemaCache[version] = (async () => {
// Try to get schema from flow-cli repo based on the flow-cli version
return fetch(GET_FLOW_SCHEMA_URL(version)).then(async (response: Response) => {
if (!response.ok) {
throw new Error(`Failed to fetch schema from flow-cli repo: ${response.statusText}`)
}
return await response.text()
}).catch(async () => {
const schemaUrl = resolve(this.#extensionPath, 'flow-schema.json')
return await promisify(readFile)(schemaUrl).then(x => x.toString())
})
})()
}
return this.#schemaCache[version]
}

async readFile (uri: vscode.Uri): Promise<Uint8Array> {
if (uri.path === '/flow.json') {
const schema = await this.#flowSchema.getValue()
if (this.#showLocalError) {
void vscode.window.showWarningMessage('Failed to fetch flow.json schema from flow-cli repo, using local schema instead. Please update flow-cli to the latest version to get the latest schema.')
this.#showLocalError = false
}
return Buffer.from(schema, 'utf-8')
const schema = await this.#getFlowSchema()
return Buffer.from(schema)
} else {
throw new Error('Unknown schema')
}
Expand All @@ -60,7 +70,7 @@ export class JSONSchemaProvider implements vscode.FileSystemProvider, vscode.Dis
type: vscode.FileType.File,
ctime: 0,
mtime: 0,
size: await this.#flowSchema.getValue().then(x => x.length)
size: await this.#getFlowSchema().then(x => x.length)
}
} else {
throw new Error('Unknown schema')
Expand All @@ -71,27 +81,6 @@ export class JSONSchemaProvider implements vscode.FileSystemProvider, vscode.Dis
if (this.#contentProviderDisposable != null) {
this.#contentProviderDisposable.dispose()
}
this.#flowVersionSubscription.unsubscribe()
}

async #resolveFlowSchema (): Promise<string> {
return await this.#flowVersion.getValue().then(async (cliVersion) => {
// Verify that version is valid (could be null if flow-cli is not installed, etc.)
if (cliVersion == null) throw new Error('Failed to get flow-cli version, please make sure flow-cli is installed and in your PATH')

// Try to get schema from flow-cli repo based on the flow-cli version
return fetch(GET_FLOW_SCHEMA_URL(cliVersion))
}).then(async (response: Response) => {
if (!response.ok) {
throw new Error(`Failed to fetch schema from flow-cli repo: ${response.statusText}`)
}
return await response.text()
}).catch(async () => {
// Fallback to local schema
this.#showLocalError = true
const schemaUrl = resolve(this.#extensionPath, 'flow-schema.json')
return await promisify(readFile)(schemaUrl).then(x => x.toString())
})
}

// Unsupported file system provider methods
Expand Down
2 changes: 1 addition & 1 deletion extension/src/server/flow-config.ts
Expand Up @@ -172,7 +172,7 @@ export class FlowConfig implements Disposable {

// Watch and reload flow configuration when changed.
#watchWorkspaceConfiguration (): Subscription {
return this.#settings.watch$(config => config.customConfigPath).subscribe(() => {
return this.#settings.settings$(config => config.customConfigPath).subscribe(() => {
void this.reloadConfigPath()
})
}
Expand Down
9 changes: 6 additions & 3 deletions extension/src/server/language-server.ts
Expand Up @@ -3,7 +3,7 @@ import { window } from 'vscode'
import { Settings } from '../settings/settings'
import { exec } from 'child_process'
import { ExecuteCommandRequest } from 'vscode-languageclient'
import { BehaviorSubject, Subscription, filter, firstValueFrom } from 'rxjs'
import { BehaviorSubject, Subscription, filter, firstValueFrom, skip, zip } from 'rxjs'
import { envVars } from '../utils/shell/env-vars'
import { FlowConfig } from './flow-config'
import { CliProvider } from '../flow-cli/cli-provider'
Expand Down Expand Up @@ -156,8 +156,11 @@ export class LanguageServerAPI {
void this.restart()
}

this.#subscriptions.push(this.#cliProvider.currentBinary$.subscribe(onChange))
this.#subscriptions.push(this.#settings.watch$((config) => config.accessCheckMode).subscribe(onChange))
const subscription = zip(
this.#cliProvider.currentBinary$.pipe(skip(1)),
this.#settings.settings$((config) => config.flowCommand).pipe(skip(1))
).subscribe(onChange)
this.#subscriptions.push(subscription)
}

async #sendRequest (cmd: string, args: any[] = []): Promise<any> {
Expand Down

0 comments on commit f53c020

Please sign in to comment.