Skip to content

Commit

Permalink
Merge branch 'master' into nialexsan/update-packages
Browse files Browse the repository at this point in the history
  • Loading branch information
jribbink committed Sep 17, 2024
2 parents d02a0ef + d7fda1f commit fc4e95f
Show file tree
Hide file tree
Showing 40 changed files with 3,523 additions and 1,229 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/add-issues-to-devx-project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
name: Add issue to project
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@v0.5.0
- uses: actions/add-to-project@v1.0.2
with:
project-url: https://github.com/orgs/onflow/projects/13
github-token: ${{ secrets.GH_ACTION_FOR_PROJECTS }}
32 changes: 32 additions & 0 deletions .metadata/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Extension metadata

**DO NOT DELETE THIS FOLDER UNLESS YOU KNOW WHAT YOU ARE DOING**

This folder contains remotely-updated metadata to provide updates to the Cadence VSCode Extension without requiring a new release of the extension itself. When consuming this metadata, the latest commit to the default repository branch should be assumed to be the latest version of the extension metadata.

Currently, it is only used by the Cadence VSCode Extension to fetch any notifications that should be displayed to the user.

## Notfications schema

```ts
interface Notification {
_type: 'Notification'
id: string
type: 'error' | 'info' | 'warning'
text: string
buttons?: Array<{
label: string
link: string
}>
suppressable?: boolean
}
```

### Fields

- `_type`: The type of the object. Should always be `"Notification"`.
- `id`: A unique identifier for the notification. This is used to determine if the notification has already been displayed to the user.
- `type`: The type of notification. Can be `"info"`, `"warning"`, or `"error"`.
- `text`: The text to display to the user.
- `buttons`: An array of buttons to display to the user. Each button should have a `label` field and a `link` field. The `link` field should be a URL to open when the button is clicked.
- `suppressable`: Whether or not the user should be able to suppress the notification. (defaults to `true`)
1 change: 1 addition & 0 deletions .metadata/notifications.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
1 change: 0 additions & 1 deletion extension/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
declare module '@onflow/decode'
declare module 'portscanner-sync'
declare module 'elliptic'
declare module 'node-fetch'
24 changes: 5 additions & 19 deletions extension/src/crypto-polyfill.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,7 @@
import * as crypto from 'crypto'

globalThis.crypto = {
getRandomValues: (buffer: Buffer) => {
const buf = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)
if (!(buf instanceof Uint8Array)) {
throw new TypeError('expected Uint8Array')
}
if (buf.length > 65536) {
const e = new Error()
// @ts-expect-error
e.code = 22
e.message = `Failed to execute 'getRandomValues' on 'Crypto': The 'ArrayBufferView's byte length (${buf.length}) exceeds the number of bytes of entropy available via this API (65536).`
e.name = 'QuotaExceededError'
throw e
}
const bytes = crypto.randomBytes(buf.length)
buf.set(bytes)
return buf
}
} as any
if (globalThis.crypto == null) {
Object.defineProperty(globalThis, 'crypto', {
value: crypto
})
}
21 changes: 14 additions & 7 deletions extension/src/dependency-installer/dependency-installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Installer, InstallerConstructor, InstallerContext, InstallError } from
import { promptUserErrorMessage } from '../ui/prompts'
import { StateCache } from '../utils/state-cache'
import { LanguageServerAPI } from '../server/language-server'
import { FlowVersionProvider } from '../flow-cli/flow-version-provider'
import { CliProvider } from '../flow-cli/cli-provider'

const INSTALLERS: InstallerConstructor[] = [
InstallFlowCLI
Expand All @@ -15,12 +15,14 @@ export class DependencyInstaller {
missingDependencies: StateCache<Installer[]>
#installerContext: InstallerContext

constructor (languageServerApi: LanguageServerAPI, flowVersionProvider: FlowVersionProvider) {
constructor (languageServerApi: LanguageServerAPI, cliProvider: CliProvider) {
this.#installerContext = {
refreshDependencies: this.checkDependencies.bind(this),
languageServerApi,
flowVersionProvider
cliProvider
}

// Register installers
this.#registerInstallers()

// Create state cache for missing dependencies
Expand All @@ -40,8 +42,12 @@ export class DependencyInstaller {
// Prompt user to install missing dependencies
promptUserErrorMessage(
'Not all dependencies are installed: ' + missing.map(x => x.getName()).join(', '),
'Install Missing Dependencies',
() => { void this.#installMissingDependencies() }
[
{
label: 'Install Missing Dependencies',
callback: () => { void this.#installMissingDependencies() }
}
]
)
}
})
Expand All @@ -50,7 +56,8 @@ export class DependencyInstaller {
async checkDependencies (): Promise<void> {
// Invalidate and wait for state to update
// This will trigger the missingDependencies subscriptions
await this.missingDependencies.getValue(true)
this.missingDependencies.invalidate()
await this.missingDependencies.getValue()
}

async installMissing (): Promise<void> {
Expand All @@ -76,7 +83,7 @@ export class DependencyInstaller {
const missing = await this.missingDependencies.getValue()
const installed: Installer[] = this.registeredInstallers.filter(x => !missing.includes(x))

await new Promise<void>((resolve, reject) => {
await new Promise<void>((resolve) => {
setTimeout(() => { resolve() }, 2000)
})

Expand Down
4 changes: 2 additions & 2 deletions extension/src/dependency-installer/installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
import { window } from 'vscode'
import { envVars } from '../utils/shell/env-vars'
import { LanguageServerAPI } from '../server/language-server'
import { FlowVersionProvider } from '../flow-cli/flow-version-provider'
import { CliProvider } from '../flow-cli/cli-provider'

// InstallError is thrown if install fails
export class InstallError extends Error {}

export interface InstallerContext {
refreshDependencies: () => Promise<void>
languageServerApi: LanguageServerAPI
flowVersionProvider: FlowVersionProvider
cliProvider: CliProvider
}

export type InstallerConstructor = new (context: InstallerContext) => Installer
Expand Down
80 changes: 49 additions & 31 deletions extension/src/dependency-installer/installers/flow-cli-installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@ import { Installer, InstallerConstructor, InstallerContext } from '../installer'
import * as semver from 'semver'
import fetch from 'node-fetch'
import { HomebrewInstaller } from './homebrew-installer'
import { KNOWN_FLOW_COMMANDS } from '../../flow-cli/cli-versions-provider'

// Relevant subset of Homebrew formulae JSON
interface HomebrewVersionInfo {
versions: {
stable: string
}
}

// Command to check flow-cli
const COMPATIBLE_FLOW_CLI_VERSIONS = '>=1.6.0'
const COMPATIBLE_FLOW_CLI_VERSIONS = '>=2.0.0'

// Shell install commands
const BREW_INSTALL_FLOW_CLI = 'brew update && brew install flow-cli'
Expand All @@ -20,7 +28,8 @@ const BASH_INSTALL_FLOW_CLI = (githubToken?: string): string =>
`${
githubToken != null ? `GITHUB_TOKEN=${githubToken} ` : ''
}sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)"`
const VERSION_INFO_URL = 'https://raw.githubusercontent.com/onflow/flow-cli/master/version.txt'
const VERSION_INFO_URL = 'https://formulae.brew.sh/api/formula/flow-cli.json'

export class InstallFlowCLI extends Installer {
#githubToken: string | undefined
#context: InstallerContext
Expand Down Expand Up @@ -77,54 +86,63 @@ export class InstallFlowCLI extends Installer {
await execVscodeTerminal('Install Flow CLI', BASH_INSTALL_FLOW_CLI())
}

async findLatestVersion (currentVersion: semver.SemVer): Promise<void> {
const response = await fetch(VERSION_INFO_URL)
const latestStr = semver.clean(await response.text())
const latest: semver.SemVer | null = semver.parse(latestStr)

// Check if latest version > current version
if (latest != null && latestStr != null && semver.compare(latest, currentVersion) === 1) {
promptUserInfoMessage(
'There is a new Flow CLI version available: ' + latestStr,
'Install latest Flow CLI',
async () => {
await this.runInstall()
await this.#context.refreshDependencies()
}
)
}
async maybeNotifyNewerVersion (currentVersion: semver.SemVer): Promise<void> {
try {
const response = await fetch(VERSION_INFO_URL)
const { versions: { stable: latestStr } }: HomebrewVersionInfo = await response.json()
const latest: semver.SemVer | null = semver.parse(latestStr)

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

async checkVersion (vsn?: semver.SemVer): Promise<boolean> {
async checkVersion (version: semver.SemVer): Promise<boolean> {
// Get user's version informaton
this.#context.flowVersionProvider.refresh()
const version = vsn ?? await this.#context.flowVersionProvider.getVersion()
if (version === null) return false
this.#context.cliProvider.refresh()
if (version == null) return false

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

// Check for newer version
await this.findLatestVersion(version)
// Maybe notify user of newer version, non-blocking
void this.maybeNotifyNewerVersion(version)

return true
}

async verifyInstall (): Promise<boolean> {
// Check if flow version is valid to verify install
this.#context.flowVersionProvider.refresh()
const version = await this.#context.flowVersionProvider.getVersion()
this.#context.cliProvider.refresh()
const installedVersions = await this.#context.cliProvider.getBinaryVersions().catch((e) => {
void window.showErrorMessage(`Failed to check CLI version: ${String(e.message)}`)
return []
})
const version = installedVersions.find(y => y.command === KNOWN_FLOW_COMMANDS.DEFAULT)?.version
if (version == null) return false

// Check flow-cli version number
Expand Down
50 changes: 31 additions & 19 deletions extension/src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,70 @@
/* The extension */
import './crypto-polyfill'

import { CommandController } from './commands/command-controller'
import { ExtensionContext } from 'vscode'
import { DependencyInstaller } from './dependency-installer/dependency-installer'
import { Settings } from './settings/settings'
import { FlowVersionProvider } from './flow-cli/flow-version-provider'
import { CliProvider } from './flow-cli/cli-provider'
import { JSONSchemaProvider } from './json-schema-provider'
import { LanguageServerAPI } from './server/language-server'
import { FlowConfig } from './server/flow-config'
import { TestProvider } from './test-provider/test-provider'
import { StorageProvider } from './storage/storage-provider'
import * as path from 'path'

import './crypto-polyfill'
import { NotificationProvider } from './ui/notification-provider'
import { CliSelectionProvider } from './flow-cli/cli-selection-provider'

// The container for all data relevant to the extension.
export class Extension {
// The extension singleton
static #instance: Extension
static initialized = false

static initialize (settings: Settings, ctx?: ExtensionContext): Extension {
static initialize (settings: Settings, ctx: ExtensionContext): Extension {
Extension.#instance = new Extension(settings, ctx)
Extension.initialized = true
return Extension.#instance
}

ctx: ExtensionContext | undefined
ctx: ExtensionContext
languageServer: LanguageServerAPI
#dependencyInstaller: DependencyInstaller
#commands: CommandController
#testProvider?: TestProvider
#testProvider: TestProvider
#schemaProvider: JSONSchemaProvider
#cliSelectionProvider: CliSelectionProvider

private constructor (settings: Settings, ctx: ExtensionContext | undefined) {
private constructor (settings: Settings, ctx: ExtensionContext) {
this.ctx = ctx

// Register Flow version provider
const flowVersionProvider = new FlowVersionProvider(settings)
// Initialize Storage Provider
const storageProvider = new StorageProvider(ctx?.globalState)

// Display any notifications from remote server
const notificationProvider = new NotificationProvider(storageProvider)
notificationProvider.activate()

// Register CliProvider
const cliProvider = new CliProvider(settings)

// Register CliSelectionProvider
this.#cliSelectionProvider = new CliSelectionProvider(cliProvider)

// Register JSON schema provider
if (ctx != null) JSONSchemaProvider.register(ctx, flowVersionProvider.state$)
this.#schemaProvider = new JSONSchemaProvider(ctx.extensionPath, cliProvider)

// Initialize Flow Config
const flowConfig = new FlowConfig(settings)
void flowConfig.activate()

// Initialize Language Server
this.languageServer = new LanguageServerAPI(settings, flowConfig)
this.languageServer = new LanguageServerAPI(settings, cliProvider, flowConfig)

// Check for any missing dependencies
// The language server will start if all dependencies are installed
// Otherwise, the language server will not start and will start after
// the user installs the missing dependencies
this.#dependencyInstaller = new DependencyInstaller(this.languageServer, flowVersionProvider)
this.#dependencyInstaller = new DependencyInstaller(this.languageServer, cliProvider)
this.#dependencyInstaller.missingDependencies.subscribe((missing) => {
if (missing.length === 0) {
void this.languageServer.activate()
Expand All @@ -63,18 +77,16 @@ export class Extension {
this.#commands = new CommandController(this.#dependencyInstaller)

// Initialize TestProvider
const extensionPath = ctx?.extensionPath ?? ''
const extensionPath = ctx.extensionPath ?? ''
const parserLocation = path.resolve(extensionPath, 'out/extension/cadence-parser.wasm')
this.#testProvider = new TestProvider(parserLocation, settings, flowConfig)
}

// Called on exit
async deactivate (): Promise<void> {
await this.languageServer.deactivate()
this.#testProvider?.dispose()
}

async executeCommand (command: string): Promise<boolean> {
return await this.#commands.executeCommand(command)
this.#testProvider.dispose()
this.#schemaProvider.dispose()
this.#cliSelectionProvider.dispose()
}
}
Loading

0 comments on commit fc4e95f

Please sign in to comment.