From 370f2203b229d29ebe82f74fa756a866a2200ccd Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 16 Nov 2023 08:47:36 -0800 Subject: [PATCH] Ensure LS client stops when installing Flow CLI & add tests (#478) --- .../dependency-installer.ts | 16 +- .../src/dependency-installer/installer.ts | 12 +- .../installers/flow-cli-installer.ts | 19 +- extension/src/extension.ts | 17 +- extension/src/server/language-server.ts | 2 + .../test/integration/0 - dependencies.test.ts | 52 +++++- .../integration/1 - language-server.test.ts | 23 ++- package-lock.json | 176 ++++++++++++++++++ package.json | 2 + 9 files changed, 294 insertions(+), 25 deletions(-) diff --git a/extension/src/dependency-installer/dependency-installer.ts b/extension/src/dependency-installer/dependency-installer.ts index 696007f6..418de0e5 100644 --- a/extension/src/dependency-installer/dependency-installer.ts +++ b/extension/src/dependency-installer/dependency-installer.ts @@ -1,18 +1,24 @@ import { window } from 'vscode' import { InstallFlowCLI } from './installers/flow-cli-installer' -import { Installer, InstallError } from './installer' +import { Installer, InstallerConstructor, InstallerContext, InstallError } from './installer' import { promptUserErrorMessage } from '../ui/prompts' import { StateCache } from '../utils/state-cache' +import { LanguageServerAPI } from '../server/language-server' -const INSTALLERS: Array Promise) => Installer> = [ +const INSTALLERS: InstallerConstructor[] = [ InstallFlowCLI ] export class DependencyInstaller { registeredInstallers: Installer[] = [] missingDependencies: StateCache + #installerContext: InstallerContext - constructor () { + constructor (languageServer: LanguageServerAPI) { + this.#installerContext = { + refreshDependencies: this.checkDependencies.bind(this), + langaugeServerApi: languageServer + } this.#registerInstallers() // Create state cache for missing dependencies @@ -51,13 +57,13 @@ export class DependencyInstaller { #registerInstallers (): void { // Recursively register installers and their dependencies in the correct order - (function registerInstallers (this: DependencyInstaller, installers: Array Promise) => Installer>) { + (function registerInstallers (this: DependencyInstaller, installers: InstallerConstructor[]) { installers.forEach((_installer) => { // Check if installer is already registered if (this.registeredInstallers.find(x => x instanceof _installer) != null) { return } // Register installer and dependencies - const installer = new _installer(this.checkDependencies.bind(this)) + const installer = new _installer(this.#installerContext) registerInstallers.bind(this)(installer.dependencies) this.registeredInstallers.push(installer) }) diff --git a/extension/src/dependency-installer/installer.ts b/extension/src/dependency-installer/installer.ts index 2f40fc42..6484cf88 100644 --- a/extension/src/dependency-installer/installer.ts +++ b/extension/src/dependency-installer/installer.ts @@ -1,15 +1,23 @@ /* Abstract Installer class */ import { window } from 'vscode' import { envVars } from '../utils/shell/env-vars' +import { LanguageServerAPI } from '../server/language-server' // InstallError is thrown if install fails export class InstallError extends Error {} +export interface InstallerContext { + refreshDependencies: () => Promise + langaugeServerApi: LanguageServerAPI +} + +export type InstallerConstructor = new (context: InstallerContext) => Installer + export abstract class Installer { - dependencies: Array Promise) => Installer> + dependencies: InstallerConstructor[] #installerName: string - constructor (name: string, dependencies: Array Promise) => Installer>) { + constructor (name: string, dependencies: InstallerConstructor[]) { this.dependencies = dependencies this.#installerName = name } diff --git a/extension/src/dependency-installer/installers/flow-cli-installer.ts b/extension/src/dependency-installer/installers/flow-cli-installer.ts index 23789f41..99b98bba 100644 --- a/extension/src/dependency-installer/installers/flow-cli-installer.ts +++ b/extension/src/dependency-installer/installers/flow-cli-installer.ts @@ -2,10 +2,9 @@ import { window } from 'vscode' import { execVscodeTerminal, tryExecPowerShell, tryExecUnixDefault } from '../../utils/shell/exec' import { promptUserInfoMessage, promptUserErrorMessage } from '../../ui/prompts' -import { Installer } from '../installer' +import { Installer, InstallerConstructor, InstallerContext } from '../installer' import * as semver from 'semver' import fetch from 'node-fetch' -import { ext } from '../../main' import { HomebrewInstaller } from './homebrew-installer' import { flowVersion } from '../../utils/flow-version' @@ -25,21 +24,23 @@ const BASH_INSTALL_FLOW_CLI = (githubToken?: string): string => const VERSION_INFO_URL = 'https://raw.githubusercontent.com/onflow/flow-cli/master/version.txt' export class InstallFlowCLI extends Installer { #githubToken: string | undefined + #context: InstallerContext - constructor (private readonly refreshDependencies: () => Promise) { + constructor (context: InstallerContext) { // Homebrew is a dependency for macos and linux - const dependencies: Array Promise) => Installer> = [] + const dependencies: InstallerConstructor[] = [] if (process.platform === 'darwin') { dependencies.push(HomebrewInstaller) } super('Flow CLI', dependencies) this.#githubToken = process.env.GITHUB_TOKEN + this.#context = context } async install (): Promise { - const isActive = ext?.languageServer.isActive ?? false - if (isActive) await ext?.languageServer.deactivate() + const isActive = this.#context.langaugeServerApi.isActive ?? false + if (isActive) await this.#context.langaugeServerApi.deactivate() const OS_TYPE = process.platform try { @@ -57,7 +58,7 @@ export class InstallFlowCLI extends Installer { } catch { void window.showErrorMessage('Failed to install Flow CLI') } - if (isActive) await ext?.languageServer.activate() + if (isActive) await this.#context.langaugeServerApi.activate() } async #install_macos (): Promise { @@ -89,7 +90,7 @@ export class InstallFlowCLI extends Installer { 'Install latest Flow CLI', async () => { await this.runInstall() - await this.refreshDependencies() + await this.#context.refreshDependencies() } ) } @@ -108,7 +109,7 @@ export class InstallFlowCLI extends Installer { 'Install latest Flow CLI', async () => { await this.runInstall() - await this.refreshDependencies() + await this.#context.refreshDependencies() } ) return false diff --git a/extension/src/extension.ts b/extension/src/extension.ts index d60f4690..a48f6ce8 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -31,18 +31,25 @@ export class Extension { // Register JSON schema provider if (ctx != null) JSONSchemaProvider.register(ctx, flowVersion) - // Initialize Language Server + // Initialize Flow Config const flowConfig = new FlowConfig(settings) + void flowConfig.activate() + + // Initialize Language Server this.languageServer = new LanguageServerAPI(settings, flowConfig) - void flowConfig.activate().then(() => { - void this.languageServer.activate() - }) // 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.#dependencyInstaller = new DependencyInstaller(this.languageServer) + this.#dependencyInstaller.missingDependencies.subscribe((missing) => { + if (missing.length === 0) { + void this.languageServer.activate() + } else { + void this.languageServer.deactivate() + } + }) // Initialize ExtensionCommands this.#commands = new CommandController(this.#dependencyInstaller) diff --git a/extension/src/server/language-server.ts b/extension/src/server/language-server.ts index 43ca96f5..e17d4709 100644 --- a/extension/src/server/language-server.ts +++ b/extension/src/server/language-server.ts @@ -27,6 +27,7 @@ export class LanguageServerAPI { } async activate (): Promise { + if (this.isActive) return await this.deactivate() this.#isActive = true @@ -38,6 +39,7 @@ export class LanguageServerAPI { this.#isActive = false this.#configPathSubscription?.unsubscribe() this.#configModifiedSubscription?.unsubscribe() + await this.stopClient() } get isActive (): boolean { diff --git a/extension/test/integration/0 - dependencies.test.ts b/extension/test/integration/0 - dependencies.test.ts index 371a6678..ee51fd61 100644 --- a/extension/test/integration/0 - dependencies.test.ts +++ b/extension/test/integration/0 - dependencies.test.ts @@ -1,15 +1,65 @@ import * as assert from 'assert' import { DependencyInstaller } from '../../src/dependency-installer/dependency-installer' import { MaxTimeout } from '../globals' +import { InstallFlowCLI } from '../../src/dependency-installer/installers/flow-cli-installer' +import { stub } from 'sinon' // Note: Dependency installation must run before other integration tests suite('Dependency Installer', () => { test('Install Missing Dependencies', async () => { - const dependencyManager = new DependencyInstaller() + const mockLangaugeServerApi = { + activate: stub(), + deactivate: stub(), + isActive: true + } + const dependencyManager = new DependencyInstaller(mockLangaugeServerApi as any) await assert.doesNotReject(async () => { await dependencyManager.installMissing() }) // Check that all dependencies are installed await dependencyManager.checkDependencies() assert.deepStrictEqual(await dependencyManager.missingDependencies.getValue(), []) }).timeout(MaxTimeout) + + test('Flow CLI installer restarts langauge server if active', async () => { + const mockLangaugeServerApi = { + activate: stub().callsFake(async () => { + mockLangaugeServerApi.isActive = true + }), + deactivate: stub().callsFake(async () => { + mockLangaugeServerApi.isActive = false + }), + isActive: true + } + const mockInstallerContext = { + refreshDependencies: async () => {}, + langaugeServerApi: mockLangaugeServerApi as any + } + 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)) + }).timeout(MaxTimeout) + + test('Flow CLI installer does not restart langauge server if inactive', async () => { + const mockLangaugeServerApi = { + activate: stub().callsFake(async () => { + mockLangaugeServerApi.isActive = true + }), + deactivate: stub().callsFake(async () => { + mockLangaugeServerApi.isActive = false + }), + isActive: false + } + const mockInstallerContext = { + refreshDependencies: async () => {}, + langaugeServerApi: mockLangaugeServerApi as any + } + const flowCliInstaller = new InstallFlowCLI(mockInstallerContext) + + await assert.doesNotReject(async () => { await flowCliInstaller.install() }) + assert(mockLangaugeServerApi.activate.notCalled) + assert(mockLangaugeServerApi.deactivate.notCalled) + }).timeout(MaxTimeout) }) diff --git a/extension/test/integration/1 - language-server.test.ts b/extension/test/integration/1 - language-server.test.ts index 29bdeb30..f5673b58 100644 --- a/extension/test/integration/1 - language-server.test.ts +++ b/extension/test/integration/1 - language-server.test.ts @@ -5,21 +5,25 @@ import { LanguageServerAPI } from '../../src/server/language-server' import { FlowConfig } from '../../src/server/flow-config' import { Settings } from '../../src/settings/settings' import { MaxTimeout } from '../globals' -import { of } from 'rxjs' +import { Subject } from 'rxjs' import { State } from 'vscode-languageclient' suite('Language Server & Emulator Integration', () => { let LS: LanguageServerAPI let settings: Settings let mockConfig: FlowConfig + let fileModified$: Subject + let pathChanged$: Subject before(async function () { this.timeout(MaxTimeout) // Initialize language server settings = getMockSettings() + fileModified$ = new Subject() + pathChanged$ = new Subject() mockConfig = { - fileModified$: of(), - pathChanged$: of(), + fileModified$, + pathChanged$, configPath: null } as any @@ -37,4 +41,17 @@ suite('Language Server & Emulator Integration', () => { assert.notStrictEqual(LS.client, undefined) assert.equal(LS.client?.state, State.Running) }) + + test('Deactivate Language Server Client', async () => { + const client = LS.client + await LS.deactivate() + + // Check that client remains stopped even if config changes + fileModified$.next() + pathChanged$.next('foo') + + assert.equal(client?.state, State.Stopped) + assert.equal(LS.client, null) + assert.equal(LS.clientState$.getValue(), State.Stopped) + }) }) diff --git a/package-lock.json b/package-lock.json index e17ddfb6..dd3a7b4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "@types/node": "^20.8.10", "@types/object-hash": "^3.0.5", "@types/semver": "^7.5.1", + "@types/sinon": "^17.0.1", "@types/uuid": "^9.0.7", "@types/vscode": "^1.65.0", "@vscode/test-electron": "^2.3.4", @@ -43,6 +44,7 @@ "nyc": "^15.1.0", "ovsx": "^0.8.3", "rimraf": "^5.0.1", + "sinon": "^17.0.1", "ts-mocha": "^10.0.0", "ts-node": "^10.9.1", "ts-standard": "^12.0.2", @@ -1235,6 +1237,50 @@ "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", "dev": true }, + "node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -1369,6 +1415,21 @@ "integrity": "sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg==", "dev": true }, + "node_modules/@types/sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-Q2Go6TJetYn5Za1+RJA1Aik61Oa2FS8SuJ0juIqUuJ5dZR4wvhKfmSdIqWtQ3P6gljKWjW0/R7FZkA4oXVL6OA==", + "dev": true, + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true + }, "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -5606,6 +5667,12 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, "node_modules/keytar": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", @@ -5688,6 +5755,12 @@ "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", "dev": true }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6221,6 +6294,46 @@ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true }, + "node_modules/nise": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz", + "integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, "node_modules/node-abi": { "version": "3.22.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.22.0.tgz", @@ -6944,6 +7057,21 @@ "node": "14 || >=16.14" } }, + "node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/path-to-regexp/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -7627,6 +7755,54 @@ "simple-concat": "^1.0.0" } }, + "node_modules/sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", diff --git a/package.json b/package.json index 86f1b65a..6fae3de7 100644 --- a/package.json +++ b/package.json @@ -181,6 +181,7 @@ "@types/node": "^20.8.10", "@types/object-hash": "^3.0.5", "@types/semver": "^7.5.1", + "@types/sinon": "^17.0.1", "@types/uuid": "^9.0.7", "@types/vscode": "^1.65.0", "@vscode/test-electron": "^2.3.4", @@ -191,6 +192,7 @@ "nyc": "^15.1.0", "ovsx": "^0.8.3", "rimraf": "^5.0.1", + "sinon": "^17.0.1", "ts-mocha": "^10.0.0", "ts-node": "^10.9.1", "ts-standard": "^12.0.2",