diff --git a/extension/src/dependency-installer/dependency-installer.ts b/extension/src/dependency-installer/dependency-installer.ts index 418de0e5..a1d72a3d 100644 --- a/extension/src/dependency-installer/dependency-installer.ts +++ b/extension/src/dependency-installer/dependency-installer.ts @@ -4,6 +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' const INSTALLERS: InstallerConstructor[] = [ InstallFlowCLI @@ -14,10 +15,11 @@ export class DependencyInstaller { missingDependencies: StateCache #installerContext: InstallerContext - constructor (languageServer: LanguageServerAPI) { + constructor (languageServerApi: LanguageServerAPI, flowVersionProvider: FlowVersionProvider) { this.#installerContext = { refreshDependencies: this.checkDependencies.bind(this), - langaugeServerApi: languageServer + languageServerApi, + flowVersionProvider } this.#registerInstallers() diff --git a/extension/src/dependency-installer/installer.ts b/extension/src/dependency-installer/installer.ts index 6484cf88..0a7d20f5 100644 --- a/extension/src/dependency-installer/installer.ts +++ b/extension/src/dependency-installer/installer.ts @@ -2,13 +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' // InstallError is thrown if install fails export class InstallError extends Error {} export interface InstallerContext { refreshDependencies: () => Promise - langaugeServerApi: LanguageServerAPI + languageServerApi: LanguageServerAPI + flowVersionProvider: FlowVersionProvider } export type InstallerConstructor = new (context: InstallerContext) => Installer diff --git a/extension/src/dependency-installer/installers/flow-cli-installer.ts b/extension/src/dependency-installer/installers/flow-cli-installer.ts index 99b98bba..43401e88 100644 --- a/extension/src/dependency-installer/installers/flow-cli-installer.ts +++ b/extension/src/dependency-installer/installers/flow-cli-installer.ts @@ -6,7 +6,6 @@ import { Installer, InstallerConstructor, InstallerContext } from '../installer' import * as semver from 'semver' import fetch from 'node-fetch' import { HomebrewInstaller } from './homebrew-installer' -import { flowVersion } from '../../utils/flow-version' // Command to check flow-cli const COMPATIBLE_FLOW_CLI_VERSIONS = '>=1.6.0' @@ -39,8 +38,8 @@ export class InstallFlowCLI extends Installer { } async install (): Promise { - const isActive = this.#context.langaugeServerApi.isActive ?? false - if (isActive) await this.#context.langaugeServerApi.deactivate() + const isActive = this.#context.languageServerApi.isActive ?? false + if (isActive) await this.#context.languageServerApi.deactivate() const OS_TYPE = process.platform try { @@ -58,7 +57,7 @@ export class InstallFlowCLI extends Installer { } catch { void window.showErrorMessage('Failed to install Flow CLI') } - if (isActive) await this.#context.langaugeServerApi.activate() + if (isActive) await this.#context.languageServerApi.activate() } async #install_macos (): Promise { @@ -98,7 +97,8 @@ export class InstallFlowCLI extends Installer { async checkVersion (vsn?: semver.SemVer): Promise { // Get user's version informaton - const version = vsn ?? await flowVersion.getValue(true) + this.#context.flowVersionProvider.refresh() + const version = vsn ?? await this.#context.flowVersionProvider.getVersion() if (version === null) return false if (!semver.satisfies(version, COMPATIBLE_FLOW_CLI_VERSIONS, { @@ -123,7 +123,8 @@ export class InstallFlowCLI extends Installer { async verifyInstall (): Promise { // Check if flow version is valid to verify install - const version = await flowVersion.getValue(true) + this.#context.flowVersionProvider.refresh() + const version = await this.#context.flowVersionProvider.getVersion() if (version == null) return false // Check flow-cli version number diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 94a46526..f4957d62 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -3,8 +3,8 @@ 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 { JSONSchemaProvider } from './json-schema-provider' -import { flowVersion } from './utils/flow-version' import { LanguageServerAPI } from './server/language-server' import { FlowConfig } from './server/flow-config' import { TestProvider } from './test-provider/test-provider' @@ -33,8 +33,11 @@ export class Extension { private constructor (settings: Settings, ctx: ExtensionContext | undefined) { this.ctx = ctx + // Register Flow version provider + const flowVersionProvider = new FlowVersionProvider(settings) + // Register JSON schema provider - if (ctx != null) JSONSchemaProvider.register(ctx, flowVersion) + if (ctx != null) JSONSchemaProvider.register(ctx, flowVersionProvider.state$) // Initialize Flow Config const flowConfig = new FlowConfig(settings) @@ -47,7 +50,7 @@ export class Extension { // 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) + this.#dependencyInstaller = new DependencyInstaller(this.languageServer, flowVersionProvider) this.#dependencyInstaller.missingDependencies.subscribe((missing) => { if (missing.length === 0) { void this.languageServer.activate() diff --git a/extension/src/flow-cli/flow-version-provider.ts b/extension/src/flow-cli/flow-version-provider.ts new file mode 100644 index 00000000..d3bf1bc0 --- /dev/null +++ b/extension/src/flow-cli/flow-version-provider.ts @@ -0,0 +1,57 @@ +import { Settings } from '../settings/settings' +import { execDefault } from '../utils/shell/exec' +import { StateCache } from '../utils/state-cache' +import * as semver from 'semver' + +const CHECK_FLOW_CLI_CMD = (flowCommand: string): string => `${flowCommand} version` + +export class FlowVersionProvider { + #settings: Settings + #stateCache: StateCache + #parseCliVersion: (buffer: Buffer | string) => string + + constructor (settings: Settings, parseCliVersion: (buffer: Buffer | string) => string = parseFlowCliVersion) { + this.#stateCache = new StateCache(async () => await this.#fetchFlowVersion()) + this.#settings = settings + this.#parseCliVersion = parseCliVersion + } + + async #fetchFlowVersion (): Promise { + try { + // Get user's version informaton + const buffer: string = (await execDefault(CHECK_FLOW_CLI_CMD( + this.#settings.getSettings().flowCommand + ))).stdout + + // Format version string from output + let versionStr: string | null = this.#parseCliVersion(buffer) + + versionStr = semver.clean(versionStr) + 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 version + } catch { + return null + } + } + + refresh (): void { + this.#stateCache.invalidate() + } + + async getVersion (): Promise { + return await this.#stateCache.getValue() + } + + get state$ (): StateCache { + return this.#stateCache + } +} + +export function parseFlowCliVersion (buffer: Buffer | string): string { + return (buffer.toString().split('\n')[0]).split(' ')[1] +} diff --git a/extension/src/utils/flow-version.ts b/extension/src/utils/flow-version.ts deleted file mode 100644 index 599c1461..00000000 --- a/extension/src/utils/flow-version.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { execDefault } from './shell/exec' -import { StateCache } from './state-cache' -import * as semver from 'semver' - -const CHECK_FLOW_CLI_CMD = 'flow version' - -async function fetchFlowVersion (): Promise { - try { - // Get user's version informaton - const buffer: string = (await execDefault(CHECK_FLOW_CLI_CMD)).stdout - - // Format version string from output - let versionStr: string | null = parseFlowCliVersion(buffer) - - versionStr = semver.clean(versionStr) - 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 version - } catch { - return null - } -} - -export function parseFlowCliVersion (buffer: Buffer | string): string { - return (buffer.toString().split('\n')[0]).split(' ')[1] -} - -export const flowVersion = new StateCache(fetchFlowVersion) diff --git a/extension/test/integration/0 - dependencies.test.ts b/extension/test/integration/0 - dependencies.test.ts index ee51fd61..674daf97 100644 --- a/extension/test/integration/0 - dependencies.test.ts +++ b/extension/test/integration/0 - dependencies.test.ts @@ -3,16 +3,25 @@ import { DependencyInstaller } from '../../src/dependency-installer/dependency-i import { MaxTimeout } from '../globals' import { InstallFlowCLI } from '../../src/dependency-installer/installers/flow-cli-installer' import { stub } from 'sinon' +import { before } from 'mocha' +import { FlowVersionProvider } from '../../src/flow-cli/flow-version-provider' +import { getMockSettings } from '../mock/mockSettings' // Note: Dependency installation must run before other integration tests suite('Dependency Installer', () => { + let flowVersionProvider: any + + before(async function () { + flowVersionProvider = new FlowVersionProvider(getMockSettings()) + }) + test('Install Missing Dependencies', async () => { - const mockLangaugeServerApi = { + const mockLanguageServerApi = { activate: stub(), deactivate: stub(), isActive: true } - const dependencyManager = new DependencyInstaller(mockLangaugeServerApi as any) + const dependencyManager = new DependencyInstaller(mockLanguageServerApi as any, flowVersionProvider) await assert.doesNotReject(async () => { await dependencyManager.installMissing() }) // Check that all dependencies are installed @@ -21,45 +30,47 @@ suite('Dependency Installer', () => { }).timeout(MaxTimeout) test('Flow CLI installer restarts langauge server if active', async () => { - const mockLangaugeServerApi = { + const mockLanguageServerApi = { activate: stub().callsFake(async () => { - mockLangaugeServerApi.isActive = true + mockLanguageServerApi.isActive = true }), deactivate: stub().callsFake(async () => { - mockLangaugeServerApi.isActive = false + mockLanguageServerApi.isActive = false }), isActive: true } const mockInstallerContext = { refreshDependencies: async () => {}, - langaugeServerApi: mockLangaugeServerApi as any + languageServerApi: mockLanguageServerApi as any, + flowVersionProvider } const flowCliInstaller = new InstallFlowCLI(mockInstallerContext) await assert.doesNotReject(async () => { await flowCliInstaller.install() }) - assert(mockLangaugeServerApi.deactivate.calledOnce) - assert(mockLangaugeServerApi.activate.calledOnce) - assert(mockLangaugeServerApi.deactivate.calledBefore(mockLangaugeServerApi.activate)) + assert(mockLanguageServerApi.deactivate.calledOnce) + assert(mockLanguageServerApi.activate.calledOnce) + assert(mockLanguageServerApi.deactivate.calledBefore(mockLanguageServerApi.activate)) }).timeout(MaxTimeout) test('Flow CLI installer does not restart langauge server if inactive', async () => { - const mockLangaugeServerApi = { + const mockLanguageServerApi = { activate: stub().callsFake(async () => { - mockLangaugeServerApi.isActive = true + mockLanguageServerApi.isActive = true }), deactivate: stub().callsFake(async () => { - mockLangaugeServerApi.isActive = false + mockLanguageServerApi.isActive = false }), isActive: false } const mockInstallerContext = { refreshDependencies: async () => {}, - langaugeServerApi: mockLangaugeServerApi as any + languageServerApi: mockLanguageServerApi as any, + flowVersionProvider } const flowCliInstaller = new InstallFlowCLI(mockInstallerContext) await assert.doesNotReject(async () => { await flowCliInstaller.install() }) - assert(mockLangaugeServerApi.activate.notCalled) - assert(mockLangaugeServerApi.deactivate.notCalled) + assert(mockLanguageServerApi.activate.notCalled) + assert(mockLanguageServerApi.deactivate.notCalled) }).timeout(MaxTimeout) }) diff --git a/extension/test/unit/parser.test.ts b/extension/test/unit/parser.test.ts index 0e903ba9..fe3ae107 100644 --- a/extension/test/unit/parser.test.ts +++ b/extension/test/unit/parser.test.ts @@ -1,8 +1,11 @@ -import { parseFlowCliVersion } from '../../src/utils/flow-version' +import * as assert from 'assert' +import { parseFlowCliVersion } from '../../src/flow-cli/flow-version-provider' +import { execDefault } from '../../src/utils/shell/exec' import { ASSERT_EQUAL } from '../globals' +import * as semver from 'semver' suite('Parsing Unit Tests', () => { - test('Flow CLI Version Parsing', async () => { + test('Flow CLI Version Parsing (buffer input)', async () => { let versionTest: Buffer = Buffer.from('Version: v0.1.0\nCommit: 0a1b2c3d') let formatted = parseFlowCliVersion(versionTest) ASSERT_EQUAL(formatted, 'v0.1.0') @@ -11,4 +14,17 @@ suite('Parsing Unit Tests', () => { formatted = parseFlowCliVersion(versionTest) ASSERT_EQUAL(formatted, 'v0.1.0') }) + + test('Flow CLI Version Parsing (string input)', async () => { + const versionTest: string = 'Version: v0.1.0\nCommit: 0a1b2c3d' + const formatted = parseFlowCliVersion(versionTest) + ASSERT_EQUAL(formatted, 'v0.1.0') + }) + + test('Flow CLI Version Parsing produces valid semver from Flow CLI output', async () => { + // Check that version is parsed from currently installed flow-cli + const { stdout } = await execDefault('flow version') + const formatted = parseFlowCliVersion(stdout) + assert(semver.valid(formatted)) + }) })