diff --git a/extensions/data-workspace/src/test/projectProviderRegistry.test.ts b/extensions/data-workspace/src/test/projectProviderRegistry.test.ts index b8751bb415..914f0b7468 100644 --- a/extensions/data-workspace/src/test/projectProviderRegistry.test.ts +++ b/extensions/data-workspace/src/test/projectProviderRegistry.test.ts @@ -8,6 +8,7 @@ import 'mocha'; import * as should from 'should'; import * as vscode from 'vscode'; import { ProjectProviderRegistry } from '../common/projectProviderRegistry'; +import { prettyPrintProviders } from './testUtils'; export class MockTreeDataProvider implements vscode.TreeDataProvider{ onDidChangeTreeData?: vscode.Event | undefined; @@ -38,6 +39,10 @@ export function createProjectProvider(projectTypes: IProjectType[], projectActio } suite('ProjectProviderRegistry Tests', function (): void { + this.beforeEach(() => { + ProjectProviderRegistry.clear(); + }); + test('register and unregister project providers', async () => { const provider1 = createProjectProvider([ { @@ -111,7 +116,7 @@ suite('ProjectProviderRegistry Tests', function (): void { columns: [{ displayName: 'c1', width: 75, type: 'string' }], data: [['d1']] }]); - should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider at the beginning of the test'); + should.strictEqual(ProjectProviderRegistry.providers.length, 0, `there should be no project provider at the beginning of the test, but found ${prettyPrintProviders()}`); const disposable1 = ProjectProviderRegistry.registerProvider(provider1, 'test.testProvider'); let providerResult = ProjectProviderRegistry.getProviderByProjectExtension('testproj'); should.equal(providerResult, provider1, 'provider1 should be returned for testproj project type'); @@ -141,7 +146,7 @@ suite('ProjectProviderRegistry Tests', function (): void { disposable2.dispose(); providerResult = ProjectProviderRegistry.getProviderByProjectExtension('sqlproj'); should.equal(providerResult, undefined, 'undefined should be returned for sqlproj project type after provider2 is disposed'); - should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider after unregistering the providers'); + should.strictEqual(ProjectProviderRegistry.providers.length, 0, `there should be no project provider after unregistering the providers, but found ${prettyPrintProviders()}`); }); test('Clear the project provider registry', async () => { @@ -163,10 +168,10 @@ suite('ProjectProviderRegistry Tests', function (): void { columns: [{ displayName: 'c1', width: 75, type: 'string' }], data: [['d1']] }]); - should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider at the beginning of the test'); + should.strictEqual(ProjectProviderRegistry.providers.length, 0, `there should be no project provider at the beginning of the test, but found ${prettyPrintProviders()}`); ProjectProviderRegistry.registerProvider(provider, 'test.testProvider'); - should.strictEqual(ProjectProviderRegistry.providers.length, 1, 'there should be only one project provider at this time'); + should.strictEqual(ProjectProviderRegistry.providers.length, 1, `there should be only one project provider at this time, but found ${prettyPrintProviders()}`); ProjectProviderRegistry.clear(); - should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider after clearing the registry'); + should.strictEqual(ProjectProviderRegistry.providers.length, 0, `there should be no project provider after clearing the registry, but found ${prettyPrintProviders()}`); }); }); diff --git a/extensions/data-workspace/src/test/testUtils.ts b/extensions/data-workspace/src/test/testUtils.ts index 4a41a92eda..8996c025eb 100644 --- a/extensions/data-workspace/src/test/testUtils.ts +++ b/extensions/data-workspace/src/test/testUtils.ts @@ -7,6 +7,7 @@ import * as os from 'os'; import * as path from 'path'; import { IProjectType } from 'dataworkspace'; import { promises as fs } from 'fs'; +import { ProjectProviderRegistry } from '../common/projectProviderRegistry'; export const testProjectType: IProjectType = { id: 'tp1', @@ -31,3 +32,7 @@ export async function createProjectFile(fileExt: string, contents?: string): Pro export function generateUniqueProjectFilePath(fileExt: string): string { return path.join(os.tmpdir(), `TestProject_${new Date().getTime()}.${fileExt}`); } + +export function prettyPrintProviders(): string { + return `${ProjectProviderRegistry.providers.map(p => `[${p.supportedProjectTypes.map(t => t.id).join(', ')}]`).join('; ')}`; +} diff --git a/extensions/data-workspace/src/test/workspaceService.test.ts b/extensions/data-workspace/src/test/workspaceService.test.ts index 267cbedbbd..6deba6214d 100644 --- a/extensions/data-workspace/src/test/workspaceService.test.ts +++ b/extensions/data-workspace/src/test/workspaceService.test.ts @@ -45,7 +45,7 @@ suite('WorkspaceService', function (): void { test('getProjectsInWorkspace', async () => { // No workspace is loaded let projects = await service.getProjectsInWorkspace(undefined, true); - should.strictEqual(projects.length, 0, 'no projects should be returned when no workspace is loaded'); + should.strictEqual(projects.length, 0, `no projects should be returned when no workspace is loaded, but found ${projects.map(p => p.fsPath).join(', ')}`); // No projects are present in the workspace file const workspaceFoldersStub = sinon.stub(vscode.workspace, 'workspaceFolders').value([]); diff --git a/extensions/sql-database-projects/package.json b/extensions/sql-database-projects/package.json index 126cb6c2b1..1508e3ddab 100644 --- a/extensions/sql-database-projects/package.json +++ b/extensions/sql-database-projects/package.json @@ -16,6 +16,7 @@ "onCommand:sqlDatabaseProjects.new", "onCommand:sqlDatabaseProjects.open", "onCommand:sqlDatabaseProjects.createProjectFromDatabase", + "onCommand:sqlDatabaseProjects.generateProjectFromOpenApiSpec", "onCommand:sqlDatabaseProjects.addSqlBinding", "workspaceContains:**/*.sqlproj", "onView:dataworkspace.views.main" @@ -44,6 +45,14 @@ "sqlDatabaseProjects.netCoreDoNotAsk": { "type": "boolean", "description": "%sqlDatabaseProjects.netCoreDoNotAsk%" + }, + "sqlDatabaseProjects.nodejsDoNotAsk": { + "type": "boolean", + "description": "%sqlDatabaseProjects.nodejsDoNotAsk%" + }, + "sqlDatabaseProjects.autorestSqlVersion": { + "type": "string", + "description": "%sqlDatabaseProjects.autorestSqlVersion%" } } } @@ -155,6 +164,11 @@ "title": "%sqlDatabaseProjects.changeTargetPlatform%", "category": "%sqlDatabaseProjects.displayName%" }, + { + "command": "sqlDatabaseProjects.generateProjectFromOpenApiSpec", + "title": "%sqlDatabaseProjects.generateProjectFromOpenApiSpec%", + "category": "%sqlDatabaseProjects.displayName%" + }, { "command": "sqlDatabaseProjects.addSqlBinding", "title": "%sqlDatabaseProjects.addSqlBinding%" @@ -165,7 +179,12 @@ { "command": "sqlDatabaseProjects.createProjectFromDatabase", "when": "view == dataworkspace.views.main", - "group": "1_currentWorkspace" + "group": "1_currentWorkspace@1" + }, + { + "command": "sqlDatabaseProjects.generateProjectFromOpenApiSpec", + "when": "view == dataworkspace.views.main", + "group": "1_currentWorkspace@2" } ], "commandPalette": [ @@ -382,18 +401,22 @@ } }, "dependencies": { - "@types/xml-formatter": "^1.1.0", "@microsoft/ads-extension-telemetry": "^1.1.5", + "@types/which": "^2.0.1", + "@types/xml-formatter": "^1.1.0", "fast-glob": "^3.1.0", "fs-extra": "^5.0.0", "jsonc-parser": "^2.3.1", "promisify-child-process": "^3.1.1", + "semver": "^7.3.5", "vscode-languageclient": "^5.3.0-next.1", "vscode-nls": "^4.1.2", + "which": "^2.0.2", "xml-formatter": "^2.1.0", "xmldom": "^0.3.0" }, "devDependencies": { + "@types/fs-extra": "^5.0.0", "@types/mocha": "^5.2.5", "@types/sinon": "^9.0.4", "@types/xmldom": "^0.1.29", @@ -405,8 +428,7 @@ "tslint": "^5.8.0", "typemoq": "^2.1.0", "typescript": "^2.6.1", - "vscodetestcover": "^1.1.0", - "@types/fs-extra": "^5.0.0" + "vscodetestcover": "^1.1.0" }, "__metadata": { "id": "70", diff --git a/extensions/sql-database-projects/package.nls.json b/extensions/sql-database-projects/package.nls.json index 32be34be63..f64571ad6d 100644 --- a/extensions/sql-database-projects/package.nls.json +++ b/extensions/sql-database-projects/package.nls.json @@ -30,10 +30,13 @@ "sqlDatabaseProjects.openContainingFolder": "Open Containing Folder", "sqlDatabaseProjects.editProjectFile": "Edit .sqlproj File", "sqlDatabaseProjects.changeTargetPlatform": "Change Target Platform", + "sqlDatabaseProjects.generateProjectFromOpenApiSpec": "Generate SQL Project from OpenAPI/Swagger spec", "sqlDatabaseProjects.Settings": "Database Projects", "sqlDatabaseProjects.netCoreInstallLocation": "Full path to .NET Core SDK on the machine.", "sqlDatabaseProjects.netCoreDoNotAsk": "Whether to prompt the user to install .NET Core when not detected.", + "sqlDatabaseProjects.nodejsDoNotAsk": "Whether to prompt the user to install Node.js when not detected.", + "sqlDatabaseProjects.autorestSqlVersion": "Which version of Autorest.Sql to use from NPM. Latest will be used if not set.", "sqlDatabaseProjects.welcome": "No database projects currently open.\n[New Project](command:sqlDatabaseProjects.new)\n[Open Project](command:sqlDatabaseProjects.open)\n[Create Project From Database](command:sqlDatabaseProjects.importDatabase)", "sqlDatabaseProjects.addSqlBinding":"Add SQL Binding" diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 983ada9305..50f603865f 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -13,6 +13,7 @@ const localize = nls.loadMessageBundle(); export const dataSourcesFileName = 'datasources.json'; export const sqlprojExtension = '.sqlproj'; export const sqlFileExtension = '.sql'; +export const yamlFileExtension = '.yaml'; export const schemaCompareExtensionId = 'microsoft.schema-compare'; export const master = 'master'; export const masterDacpac = 'master.dacpac'; @@ -306,10 +307,12 @@ export const postDeployScriptFriendlyName = localize('postDeployScriptFriendlyNa export const NetCoreInstallationConfirmation: string = localize('sqlDatabaseProjects.NetCoreInstallationConfirmation', "The .NET Core SDK cannot be located. Project build will not work. Please install .NET Core SDK version 3.1 or update the .NET Core SDK location in settings if already installed."); export function NetCoreSupportedVersionInstallationConfirmation(installedVersion: string) { return localize('sqlDatabaseProjects.NetCoreSupportedVersionInstallationConfirmation', "Currently installed .NET Core SDK version is {0}, which is not supported. Project build will not work. Please install .NET Core SDK version 3.1 or update the .NET Core SDK supported version location in settings if already installed.", installedVersion); } export const UpdateNetCoreLocation: string = localize('sqlDatabaseProjects.UpdateNetCoreLocation', "Update Location"); -export const InstallNetCore: string = localize('sqlDatabaseProjects.InstallNetCore', "Install"); -export const DoNotAskAgain: string = localize('sqlDatabaseProjects.doNotAskAgain', "Don't Ask Again"); export const projectsOutputChannel = localize('sqlDatabaseProjects.outputChannel', "Database Projects"); +// Prompt buttons +export const Install: string = localize('sqlDatabaseProjects.Install', "Install"); +export const DoNotAskAgain: string = localize('sqlDatabaseProjects.doNotAskAgain', "Don't Ask Again"); + // SqlProj file XML names export const ItemGroup = 'ItemGroup'; export const Build = 'Build'; @@ -408,6 +411,14 @@ export enum DatabaseProjectItemType { dataSourceRoot = 'databaseProject.itemType.dataSourceRoot', } +// AutoRest +export const autorestPostDeploymentScriptName = 'PostDeploymentScript.sql'; +export const nodeButNotAutorestFound = localize('nodeButNotAutorestFound', "Autorest tool not found in system path, but found Node.js. Running via npx. Please execute 'npm install autorest -g' to install permanently."); +export const nodeNotFound = localize('nodeNotFound', "Neither autorest nor Node.js (npx) found in system path. Please install Node.js for autorest generation to work."); +export const selectSpecFile = localize('selectSpecFile', "Select OpenAPI/Swagger spec file"); +export const generatingProjectFailed = localize('generatingProjectFailed', "Generating project via AutoRest failed"); +export function multipleMostDeploymentScripts(count: number) { return localize('multipleMostDeploymentScripts', "Unexpected number of {0} files: {1}", autorestPostDeploymentScriptName, count); } + // System dbs export const systemDbs = ['master', 'msdb', 'tempdb', 'model']; diff --git a/extensions/sql-database-projects/src/common/utils.ts b/extensions/sql-database-projects/src/common/utils.ts index 8ac65b94c7..8ab125bc0b 100644 --- a/extensions/sql-database-projects/src/common/utils.ts +++ b/extensions/sql-database-projects/src/common/utils.ts @@ -12,10 +12,11 @@ import * as glob from 'fast-glob'; import * as dataworkspace from 'dataworkspace'; import * as mssql from '../../../mssql'; import * as vscodeMssql from 'vscode-mssql'; -import { promises as fs } from 'fs'; -import { Project } from '../models/project'; import * as childProcess from 'child_process'; import * as fse from 'fs-extra'; +import * as which from 'which'; +import { promises as fs } from 'fs'; +import { Project } from '../models/project'; export interface ValidationResult { errorMessage: string; @@ -489,6 +490,23 @@ export async function retry( return undefined; } +/** + * Detects whether the specified command-line command is available on the current machine + */ +export async function detectCommandInstallation(command: string): Promise { + try { + const found = await which(command); + + if (found) { + return true; + } + } catch (err) { + console.log(getErrorMessage(err)); + } + + return false; +} + /** * Gets all the projects of the specified extension in the folder * @param folder diff --git a/extensions/sql-database-projects/src/controllers/mainController.ts b/extensions/sql-database-projects/src/controllers/mainController.ts index cd7ee564c6..acce729944 100644 --- a/extensions/sql-database-projects/src/controllers/mainController.ts +++ b/extensions/sql-database-projects/src/controllers/mainController.ts @@ -57,6 +57,7 @@ export default class MainController implements vscode.Disposable { vscode.commands.registerCommand('sqlDatabaseProjects.publish', async (node: WorkspaceTreeItem) => { this.projectsController.publishProject(node); }); vscode.commands.registerCommand('sqlDatabaseProjects.schemaCompare', async (node: WorkspaceTreeItem) => { return this.projectsController.schemaCompare(node); }); vscode.commands.registerCommand('sqlDatabaseProjects.createProjectFromDatabase', async (context: azdataType.IConnectionProfile | vscodeMssql.ITreeNodeInfo | undefined) => { return this.projectsController.createProjectFromDatabase(context); }); + vscode.commands.registerCommand('sqlDatabaseProjects.generateProjectFromOpenApiSpec', async () => { return this.projectsController.generateProjectFromOpenApiSpec(); }); vscode.commands.registerCommand('sqlDatabaseProjects.newScript', async (node: WorkspaceTreeItem) => { return this.projectsController.addItemPromptFromNode(node, templates.script); }); vscode.commands.registerCommand('sqlDatabaseProjects.newPreDeploymentScript', async (node: WorkspaceTreeItem) => { return this.projectsController.addItemPromptFromNode(node, templates.preDeployScript); }); diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index 81964703d7..277c6d255f 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -24,7 +24,8 @@ import { IDeploySettings } from '../models/IDeploySettings'; import { BaseProjectTreeItem } from '../models/tree/baseTreeItem'; import { ProjectRootTreeItem } from '../models/tree/projectTreeItem'; import { ImportDataModel } from '../models/api/import'; -import { NetCoreTool, DotNetCommandOptions, DotNetError } from '../tools/netcoreTool'; +import { NetCoreTool, DotNetError } from '../tools/netcoreTool'; +import { ShellCommandOptions } from '../tools/shellExecutionHelper'; import { BuildHelper } from '../tools/buildHelper'; import { readPublishProfile } from '../models/publishProfile/publishProfile'; import { AddDatabaseReferenceDialog } from '../dialogs/addDatabaseReferenceDialog'; @@ -38,6 +39,7 @@ import { launchPublishDatabaseQuickpick } from '../dialogs/publishDatabaseQuickp import { launchPublishToDockerContainerQuickpick } from '../dialogs/deployDatabaseQuickpick'; import { DeployService } from '../models/deploy/deployService'; import { SqlTargetPlatform } from 'sqldbproj'; +import { AutorestHelper } from '../tools/autorestHelper'; import { createNewProjectFromDatabaseWithQuickpick } from '../dialogs/createProjectFromDatabaseQuickpick'; import { addDatabaseReferenceQuickpick } from '../dialogs/addDatabaseReferenceQuickpick'; @@ -67,6 +69,7 @@ export class ProjectsController { private buildInfo: DashboardData[] = []; private publishInfo: PublishData[] = []; private deployService: DeployService; + private autorestHelper: AutorestHelper; projFileWatchers = new Map(); @@ -74,6 +77,7 @@ export class ProjectsController { this.netCoreTool = new NetCoreTool(outputChannel); this.buildHelper = new BuildHelper(); this.deployService = new DeployService(outputChannel); + this.autorestHelper = new AutorestHelper(outputChannel); } public getDashboardPublishData(projectFile: string): (string | dataworkspace.IconCellValue)[][] { @@ -215,7 +219,7 @@ export class ProjectsController { // Check mssql extension for project dlls (tracking issue #10273) await this.buildHelper.createBuildDirFolder(); - const options: DotNetCommandOptions = { + const options: ShellCommandOptions = { commandTitle: 'Build', workingDirectory: project.projectFolderPath, argument: this.buildHelper.constructBuildArguments(project.projectFilePath, this.buildHelper.extensionBuildDirPath) @@ -827,6 +831,171 @@ export class ProjectsController { return result; } + public async selectAutorestSpecFile(): Promise { + let quickpickSelection = await vscode.window.showQuickPick( + [constants.browseEllipsis], + { title: constants.selectSpecFile, ignoreFocusOut: true }); + if (!quickpickSelection) { + return; + } + + const filters: { [name: string]: string[] } = {}; + filters['OpenAPI/Swagger spec'] = ['yaml']; + + let uris = await vscode.window.showOpenDialog({ + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + openLabel: constants.selectString, + filters: filters, + title: constants.selectSpecFile + }); + + if (!uris) { + return; + } + + return uris[0].fsPath; + } + + /** + * @returns \{ newProjectFolder: 'C:\Source\MyProject', + * outputFolder: 'C:\Source', + * projectName: 'MyProject'} + */ + public async selectAutorestProjectLocation(specPath: string): Promise<{ newProjectFolder: string, outputFolder: string, projectName: string } | undefined> { + let valid = false; + let newProjectFolder: string = ''; + let outputFolder: string = ''; + let projectName: string = ''; + + let quickpickSelection = await vscode.window.showQuickPick( + [constants.browseEllipsis], + { title: constants.selectProjectLocation, ignoreFocusOut: true }); + if (!quickpickSelection) { + return; + } + + while (!valid) { + const folders = await vscode.window.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + openLabel: constants.selectString, + defaultUri: vscode.workspace.workspaceFolders?.[0]?.uri, + title: constants.selectProjectLocation + }); + + if (!folders) { + return; + } + + outputFolder = folders[0].fsPath; + projectName = path.basename(specPath, constants.yamlFileExtension); + newProjectFolder = path.join(outputFolder, projectName); + + if (await utils.exists(newProjectFolder)) { + + quickpickSelection = await vscode.window.showQuickPick( + [constants.browseEllipsis], + { title: constants.folderAlreadyExistsChooseNewLocation(newProjectFolder), ignoreFocusOut: true }); + if (!quickpickSelection) { + return; + } + } else { + valid = true; + } + } + + await fs.mkdir(newProjectFolder); + return { newProjectFolder, outputFolder, projectName }; + } + + public async generateAutorestFiles(specPath: string, newProjectFolder: string): Promise { + await this.autorestHelper.generateAutorestFiles(specPath, newProjectFolder); + } + + public async openProjectInWorkspace(projectFilePath: string): Promise { + const workspaceApi = utils.getDataWorkspaceExtensionApi(); + await workspaceApi.validateWorkspace(); + await workspaceApi.addProjectsToWorkspace([vscode.Uri.file(projectFilePath)]); + + workspaceApi.showProjectsView(); + } + + public async generateProjectFromOpenApiSpec(): Promise { + try { + // 1. select spec file + const specPath: string | undefined = await this.selectAutorestSpecFile(); + if (!specPath) { + return; + } + + // 2. select location, make new folder + const projectInfo = await this.selectAutorestProjectLocation(specPath!); + if (!projectInfo) { + return; + } + + // 3. run AutoRest to generate .sql files + await this.generateAutorestFiles(specPath, projectInfo.newProjectFolder); + + // 4. create new SQL project + const newProjFilePath = await this.createNewProject({ + newProjName: projectInfo.projectName, + folderUri: vscode.Uri.file(projectInfo.outputFolder), + projectTypeId: constants.emptySqlDatabaseProjectTypeId + }); + + const project = await Project.openProject(newProjFilePath); + + // 5. add generated files to SQL project + let fileFolderList: vscode.Uri[] = await this.getSqlFileList(project.projectFolderPath); + await project.addToProject(fileFolderList.filter(f => !f.fsPath.endsWith(constants.autorestPostDeploymentScriptName))); // Add generated file structure to the project + + const postDeploymentScript: vscode.Uri | undefined = this.findPostDeploymentScript(fileFolderList); + + if (postDeploymentScript) { + await project.addScriptItem(path.relative(project.projectFolderPath, postDeploymentScript.fsPath), undefined, templates.postDeployScript); + } + + // 6. add project to workspace and open + await this.openProjectInWorkspace(newProjFilePath); + + return project; + } catch (err) { + void vscode.window.showErrorMessage(`${constants.generatingProjectFailed}: ${utils.getErrorMessage(err)}`); + return; + } + } + + private findPostDeploymentScript(files: vscode.Uri[]): vscode.Uri | undefined { + const results = files.filter(f => f.fsPath.endsWith(constants.autorestPostDeploymentScriptName)); + + switch (results.length) { + case 0: + return undefined; + case 1: + return results[0]; + default: + throw new Error(constants.multipleMostDeploymentScripts(results.length)); + } + + } + + private async getSqlFileList(folder: string): Promise { + const entries = await fs.readdir(folder, { withFileTypes: true }); + + const folders = entries.filter(dir => dir.isDirectory()).map(dir => path.join(folder, dir.name)); + const files = entries.filter(file => !file.isDirectory() && path.extname(file.name) === '.sql').map(file => vscode.Uri.file(path.join(folder, file.name))); + + for (const folder of folders) { + files.push(...await this.getSqlFileList(folder)); + } + + return files; + } + //#region Helper methods public getPublishDialog(project: Project): PublishDatabaseDialog { diff --git a/extensions/sql-database-projects/src/test/autorestHelper.test.ts b/extensions/sql-database-projects/src/test/autorestHelper.test.ts new file mode 100644 index 0000000000..9f9192b57a --- /dev/null +++ b/extensions/sql-database-projects/src/test/autorestHelper.test.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as should from 'should'; +import * as sinon from 'sinon'; +import * as testUtils from './testUtils'; +import * as utils from '../common/utils'; +import * as path from 'path'; +import { TestContext, createContext } from './testContext'; +import { AutorestHelper } from '../tools/autorestHelper'; +import { promises as fs } from 'fs'; + +let testContext: TestContext; + +describe('Autorest tests', function (): void { + beforeEach(function (): void { + testContext = createContext(); + }); + + afterEach(function (): void { + sinon.restore(); + }); + + it('Should detect autorest', async function (): Promise { + const autorestHelper = new AutorestHelper(testContext.outputChannel); + const executable = await autorestHelper.detectInstallation(); + should(executable === 'autorest' || executable === 'npx autorest').equal(true, 'autorest command should be found in default path during unit tests'); + }); + + it('Should run an autorest command successfully', async function (): Promise { + const autorestHelper = new AutorestHelper(testContext.outputChannel); + const dummyFile = path.join(await testUtils.generateTestFolderPath(), 'testoutput.log'); + sinon.stub(autorestHelper, 'constructAutorestCommand').returns(`${await autorestHelper.detectInstallation()} --version > ${dummyFile}`); + + try { + await autorestHelper.generateAutorestFiles('fakespec.yaml', 'fakePath'); + const text = (await fs.readFile(dummyFile)).toString().trim(); + const expected = 'AutoRest code generation utility'; + should(text.includes(expected)).equal(true, `Substring not found. Expected "${expected}" in "${text}"`); + } finally { + if (await utils.exists(dummyFile)) { + await fs.unlink(dummyFile); + } + } + }); + + it('Should construct a correct autorest command for project generation', async function (): Promise { + const expectedOutput = 'autorest --use:autorest-sql-testing@latest --input-file="/some/path/test.yaml" --output-folder="/some/output/path" --clear-output-folder'; + + const autorestHelper = new AutorestHelper(testContext.outputChannel); + const constructedCommand = autorestHelper.constructAutorestCommand((await autorestHelper.detectInstallation())!, '/some/path/test.yaml', '/some/output/path'); + + // depending on whether the machine running the test has autorest installed or just node, the expected output may differ by just the prefix, hence matching against two options + should(constructedCommand === expectedOutput || constructedCommand === `npx ${expectedOutput}`).equal(true, `Constructed autorest command not formatting as expected:\nActual: ${constructedCommand}\nExpected: [npx ]${expectedOutput}`); + }); +}); diff --git a/extensions/sql-database-projects/src/test/projectController.test.ts b/extensions/sql-database-projects/src/test/projectController.test.ts index 3f86d1a46b..3099b3c46d 100644 --- a/extensions/sql-database-projects/src/test/projectController.test.ts +++ b/extensions/sql-database-projects/src/test/projectController.test.ts @@ -21,7 +21,7 @@ import { SqlDatabaseProjectTreeViewProvider } from '../controllers/databaseProje import { ProjectsController } from '../controllers/projectController'; import { promises as fs } from 'fs'; import { createContext, TestContext, mockDacFxResult, mockConnectionProfile } from './testContext'; -import { Project, reservedProjectFolders, SystemDatabase, FileProjectEntry, SystemDatabaseReferenceProjectEntry } from '../models/project'; +import { Project, reservedProjectFolders, SystemDatabase, FileProjectEntry, SystemDatabaseReferenceProjectEntry, EntryType } from '../models/project'; import { PublishDatabaseDialog } from '../dialogs/publishDatabaseDialog'; import { ProjectRootTreeItem } from '../models/tree/projectTreeItem'; import { FolderNode, FileNode } from '../models/tree/fileFolderTreeItem'; @@ -429,7 +429,7 @@ describe('ProjectsController', function (): void { const proj = await testUtils.createTestProject(baselines.openProjectFileBaseline); - await projController.object.publishOrScriptProject(proj, { connectionUri: '', databaseName: '' , serverName: ''}, false); + await projController.object.publishOrScriptProject(proj, { connectionUri: '', databaseName: '', serverName: '' }, false); should(builtDacpacPath).not.equal('', 'built dacpac path should be set'); should(publishedDacpacPath).not.equal('', 'published dacpac path should be set'); @@ -665,7 +665,7 @@ describe('ProjectsController', function (): void { // add dacpac reference to something in the a folder outside of the project await projController.addDatabaseReferenceCallback(project1, { databaseName: this.databaseNameTextbox?.value, - dacpacFileLocation: vscode.Uri.file(path.join(path.dirname(projFilePath), '..','someFolder', 'outsideFolderTest.dacpac')), + dacpacFileLocation: vscode.Uri.file(path.join(path.dirname(projFilePath), '..', 'someFolder', 'outsideFolderTest.dacpac')), suppressMissingDependenciesErrors: false }, { treeDataProvider: new SqlDatabaseProjectTreeViewProvider(), element: undefined }); @@ -678,6 +678,49 @@ describe('ProjectsController', function (): void { should(projFileText).containEql('..\\someFolder\\outsideFolderTest.dacpac'); }); }); + + describe('AutoRest generation', function (): void { + it('Should create project from autorest-generated files', async function (): Promise { + const parentFolder = await testUtils.generateTestFolderPath(); + await testUtils.createDummyFileStructure(); + const specName = 'DummySpec.yaml'; + const newProjFolder = path.join(parentFolder, path.basename(specName, '.yaml')); + let fileList: vscode.Uri[] = []; + + const projController = TypeMoq.Mock.ofType(ProjectsController); + projController.callBase = true; + + projController.setup(x => x.selectAutorestSpecFile()).returns(async () => specName); + projController.setup(x => x.selectAutorestProjectLocation(TypeMoq.It.isAny())).returns(async () => { + await fs.mkdir(newProjFolder); + + return { + newProjectFolder: newProjFolder, + outputFolder: parentFolder, + projectName: path.basename(specName, '.yaml') + }; + }); + + projController.setup(x => x.generateAutorestFiles(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(async () => { + await testUtils.createDummyFileStructure(true, fileList, newProjFolder); + await testUtils.createTestFile('SELECT \'This is a post-deployment script\'', constants.autorestPostDeploymentScriptName, newProjFolder); + }); + + projController.setup(x => x.openProjectInWorkspace(TypeMoq.It.isAny())).returns(async () => { }); + + const project = (await projController.object.generateProjectFromOpenApiSpec())!; + + should(project.postDeployScripts.length).equal(1, `Expected 1 post-deployment script, got ${project?.postDeployScripts.length}`); + const actual = path.basename(project.postDeployScripts[0].fsUri.fsPath); + should(actual).equal(constants.autorestPostDeploymentScriptName, `Unexpected post-deployment script name: ${actual}, expected ${constants.autorestPostDeploymentScriptName}`); + + const expectedScripts = fileList.filter(f => path.extname(f.fsPath) === '.sql'); + should(project.files.filter(f => f.type === EntryType.File).length).equal(expectedScripts.length, 'Unexpected number of scripts in project'); + + const expectedFolders = fileList.filter(f => path.extname(f.fsPath) === '' && f.fsPath.toUpperCase() !== newProjFolder.toUpperCase()); + should(project.files.filter(f => f.type === EntryType.Folder).length).equal(expectedFolders.length, 'Unexpected number of folders in project'); + }); + }); }); async function setupDeleteExcludeTest(proj: Project): Promise<[FileProjectEntry, ProjectRootTreeItem, FileProjectEntry, FileProjectEntry, FileProjectEntry]> { diff --git a/extensions/sql-database-projects/src/test/testUtils.ts b/extensions/sql-database-projects/src/test/testUtils.ts index b446fd2749..8a08e4cdc4 100644 --- a/extensions/sql-database-projects/src/test/testUtils.ts +++ b/extensions/sql-database-projects/src/test/testUtils.ts @@ -42,7 +42,7 @@ export async function createTestDataSources(contents: string, folderPath?: strin } export async function generateTestFolderPath(): Promise { - const folderPath = path.join(os.tmpdir(), `TestRun_${new Date().getTime()}`); + const folderPath = path.join(os.tmpdir(), 'ADS_Tests', `TestRun_${new Date().getTime()}`); await fs.mkdir(folderPath, { recursive: true }); return folderPath; diff --git a/extensions/sql-database-projects/src/test/utils.test.ts b/extensions/sql-database-projects/src/test/utils.test.ts index 0df763a5f3..50cb653995 100644 --- a/extensions/sql-database-projects/src/test/utils.test.ts +++ b/extensions/sql-database-projects/src/test/utils.test.ts @@ -7,7 +7,7 @@ import * as should from 'should'; import * as path from 'path'; import * as os from 'os'; import { createDummyFileStructure } from './testUtils'; -import { exists, trimUri, removeSqlCmdVariableFormatting, formatSqlCmdVariable, isValidSqlCmdVariableName, timeConversion, validateSqlServerPortNumber, isEmptyString } from '../common/utils'; +import { exists, trimUri, removeSqlCmdVariableFormatting, formatSqlCmdVariable, isValidSqlCmdVariableName, timeConversion, validateSqlServerPortNumber, isEmptyString, detectCommandInstallation } from '../common/utils'; import { Uri } from 'vscode'; describe('Tests to verify utils functions', function (): void { @@ -105,5 +105,10 @@ describe('Tests to verify utils functions', function (): void { should(isEmptyString(undefined)).equals(true); should(isEmptyString('65536')).equals(false); }); + + it('Should correctly detect present commands', async () => { + should(await detectCommandInstallation('node')).equal(true, '"node" should have been detected.'); + should(await detectCommandInstallation('bogusFakeCommand')).equal(false, '"bogusFakeCommand" should have been detected.'); + }); }); diff --git a/extensions/sql-database-projects/src/tools/autorestHelper.ts b/extensions/sql-database-projects/src/tools/autorestHelper.ts new file mode 100644 index 0000000000..a71eed0e47 --- /dev/null +++ b/extensions/sql-database-projects/src/tools/autorestHelper.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { DoNotAskAgain, Install, nodeButNotAutorestFound, nodeNotFound } from '../common/constants'; +import * as utils from '../common/utils'; +import * as semver from 'semver'; +import { DBProjectConfigurationKey } from './netcoreTool'; +import { ShellExecutionHelper } from './shellExecutionHelper'; + +const autorestPackageName = 'autorest-sql-testing'; // name of AutoRest.Sql package on npm +const nodejsDoNotAskAgainKey: string = 'nodejsDoNotAsk'; +const autorestSqlVersionKey: string = 'autorestSqlVersion'; + +/** + * Helper class for dealing with Autorest generation and detection + */ +export class AutorestHelper extends ShellExecutionHelper { + constructor(_outputChannel: vscode.OutputChannel) { + super(_outputChannel); + } + + /** + * Checks the workspace configuration to for an AutoRest.Sql override, otherwise latest will be used from NPM + */ + public get autorestSqlPackageVersion(): string { + let configVal: string | undefined = vscode.workspace.getConfiguration(DBProjectConfigurationKey)[autorestSqlVersionKey]; + + if (configVal && semver.valid(configVal.trim())) { + return configVal.trim(); + } else { + return 'latest'; + } + } + + /** + * @returns the beginning of the command to execute autorest; 'autorest' if available, 'npx autorest' if module not installed, or undefined if neither + */ + public async detectInstallation(): Promise { + const autorestCommand = 'autorest'; + const npxCommand = 'npx'; + + if (await utils.detectCommandInstallation(autorestCommand)) { + return autorestCommand; + } + + if (await utils.detectCommandInstallation(npxCommand)) { + this._outputChannel.appendLine(nodeButNotAutorestFound); + return `${npxCommand} ${autorestCommand}`; + } + + return undefined; + } + + /** + * Calls autorest to generate files from the spec, piping standard and error output to the host console + * @param specPath path to the OpenAPI spec file + * @param outputFolder folder in which to generate the .sql script files + * @returns console output from autorest execution + */ + public async generateAutorestFiles(specPath: string, outputFolder: string): Promise { + const commandExecutable = await this.detectInstallation(); + + if (commandExecutable === undefined) { + // unable to find autorest or npx + + if (vscode.workspace.getConfiguration(DBProjectConfigurationKey)[nodejsDoNotAskAgainKey] !== true) { + this._outputChannel.appendLine(nodeNotFound); + return; // user doesn't want to be prompted about installing it + } + + // prompt user to install Node.js + const result = await vscode.window.showErrorMessage(nodeNotFound, DoNotAskAgain, Install); + + if (result === Install) { + //open install link + const nodejsInstallationUrl = 'https://nodejs.dev/download'; + await vscode.env.openExternal(vscode.Uri.parse(nodejsInstallationUrl)); + } else if (result === DoNotAskAgain) { + const config = vscode.workspace.getConfiguration(DBProjectConfigurationKey); + await config.update(nodejsDoNotAskAgainKey, true, vscode.ConfigurationTarget.Global); + } + + return; + } + + const command = this.constructAutorestCommand(commandExecutable, specPath, outputFolder); + const output = await this.runStreamedCommand(command, this._outputChannel); + + return output; + } + + /** + * + * @param executable either "autorest" or "npx autorest", depending on whether autorest is already present in the global cache + * @param specPath path to the OpenAPI spec + * @param outputFolder folder in which to generate the files + * @returns composed command to be executed + */ + public constructAutorestCommand(executable: string, specPath: string, outputFolder: string): string { + // TODO: should --clear-output-folder be included? We should always be writing to a folder created just for this, but potentially risky + return `${executable} --use:${autorestPackageName}@${this.autorestSqlPackageVersion} --input-file="${specPath}" --output-folder="${outputFolder}" --clear-output-folder`; + } +} diff --git a/extensions/sql-database-projects/src/tools/netcoreTool.ts b/extensions/sql-database-projects/src/tools/netcoreTool.ts index ec9790a5b1..5e57709084 100644 --- a/extensions/sql-database-projects/src/tools/netcoreTool.ts +++ b/extensions/sql-database-projects/src/tools/netcoreTool.ts @@ -7,13 +7,13 @@ import * as child_process from 'child_process'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import * as cp from 'promisify-child-process'; import * as semver from 'semver'; import { isNullOrUndefined } from 'util'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; -import { DoNotAskAgain, InstallNetCore, NetCoreInstallationConfirmation, NetCoreSupportedVersionInstallationConfirmation, UpdateNetCoreLocation } from '../common/constants'; +import { DoNotAskAgain, Install, NetCoreInstallationConfirmation, NetCoreSupportedVersionInstallationConfirmation, UpdateNetCoreLocation } from '../common/constants'; import * as utils from '../common/utils'; +import { ShellCommandOptions, ShellExecutionHelper } from './shellExecutionHelper'; const localize = nls.loadMessageBundle(); export const DBProjectConfigurationKey: string = 'sqlDatabaseProjects'; @@ -33,13 +33,7 @@ export const enum netCoreInstallState { const dotnet = os.platform() === 'win32' ? 'dotnet.exe' : 'dotnet'; -export interface DotNetCommandOptions { - workingDirectory?: string; - additionalEnvironmentVariables?: NodeJS.ProcessEnv; - commandTitle?: string; - argument?: string; -} -export class NetCoreTool { +export class NetCoreTool extends ShellExecutionHelper { private osPlatform: string = os.platform(); private netCoreSdkInstalledVersion: string | undefined; @@ -61,22 +55,22 @@ export class NetCoreTool { return true; } - - constructor(private _outputChannel: vscode.OutputChannel) { + constructor(_outputChannel: vscode.OutputChannel) { + super(_outputChannel); } public async showInstallDialog(): Promise { let result; if (this.netCoreInstallState === netCoreInstallState.netCoreNotPresent) { - result = await vscode.window.showErrorMessage(NetCoreInstallationConfirmation, UpdateNetCoreLocation, InstallNetCore, DoNotAskAgain); + result = await vscode.window.showErrorMessage(NetCoreInstallationConfirmation, UpdateNetCoreLocation, Install, DoNotAskAgain); } else { - result = await vscode.window.showErrorMessage(NetCoreSupportedVersionInstallationConfirmation(this.netCoreSdkInstalledVersion!), UpdateNetCoreLocation, InstallNetCore, DoNotAskAgain); + result = await vscode.window.showErrorMessage(NetCoreSupportedVersionInstallationConfirmation(this.netCoreSdkInstalledVersion!), UpdateNetCoreLocation, Install, DoNotAskAgain); } if (result === UpdateNetCoreLocation) { //open settings await vscode.commands.executeCommand('workbench.action.openGlobalSettings'); - } else if (result === InstallNetCore) { + } else if (result === Install) { //open install link const dotnetcoreURL = 'https://dotnet.microsoft.com/download/dotnet-core/3.1'; await vscode.env.openExternal(vscode.Uri.parse(dotnetcoreURL)); @@ -183,7 +177,7 @@ export class NetCoreTool { } } - public async runDotnetCommand(options: DotNetCommandOptions): Promise { + public async runDotnetCommand(options: ShellCommandOptions): Promise { if (options && options.commandTitle !== undefined && options.commandTitle !== null) { this._outputChannel.appendLine(`\t[ ${options.commandTitle} ]`); } @@ -206,53 +200,6 @@ export class NetCoreTool { throw error; } } - - // spawns the dotnet command with arguments and redirects the error and output to ADS output channel - public async runStreamedCommand(command: string, outputChannel: vscode.OutputChannel, options?: DotNetCommandOptions): Promise { - const stdoutData: string[] = []; - outputChannel.appendLine(` > ${command}`); - - const spawnOptions = { - cwd: options && options.workingDirectory, - env: Object.assign({}, process.env, options && options.additionalEnvironmentVariables), - encoding: 'utf8', - maxBuffer: 10 * 1024 * 1024, // 10 Mb of output can be captured. - shell: true, - detached: false, - windowsHide: true - }; - - const child = cp.spawn(command, [], spawnOptions); - outputChannel.show(); - - // Add listeners to print stdout and stderr and exit code - void child.on('exit', (code: number | null, signal: string | null) => { - if (code !== null) { - outputChannel.appendLine(localize('sqlDatabaseProjects.RunStreamedCommand.ExitedWithCode', " >>> {0} … exited with code: {1}", command, code)); - } else { - outputChannel.appendLine(localize('sqlDatabaseProjects.RunStreamedCommand.ExitedWithSignal', " >>> {0} … exited with signal: {1}", command, signal)); - } - }); - - child.stdout!.on('data', (data: string | Buffer) => { - stdoutData.push(data.toString()); - this.outputDataChunk(data, outputChannel, localize('sqlDatabaseProjects.RunCommand.stdout', " stdout: ")); - }); - - child.stderr!.on('data', (data: string | Buffer) => { - this.outputDataChunk(data, outputChannel, localize('sqlDatabaseProjects.RunCommand.stderr', " stderr: ")); - }); - await child; - - return stdoutData.join(''); - } - - private outputDataChunk(data: string | Buffer, outputChannel: vscode.OutputChannel, header: string): void { - data.toString().split(/\r?\n/) - .forEach(line => { - outputChannel.appendLine(header + line); - }); - } } export class DotNetError extends Error { diff --git a/extensions/sql-database-projects/src/tools/packageHelper.ts b/extensions/sql-database-projects/src/tools/packageHelper.ts index cbae1837f6..b983b63fbe 100644 --- a/extensions/sql-database-projects/src/tools/packageHelper.ts +++ b/extensions/sql-database-projects/src/tools/packageHelper.ts @@ -6,7 +6,8 @@ import * as vscode from 'vscode'; import * as utils from '../common/utils'; import * as azureFunctionsUtils from '../common/azureFunctionsUtils'; import * as constants from '../common/constants'; -import { DotNetCommandOptions, NetCoreTool } from './netcoreTool'; +import { NetCoreTool } from './netcoreTool'; +import { ShellCommandOptions } from './shellExecutionHelper'; export class PackageHelper { private netCoreTool: NetCoreTool; @@ -40,7 +41,7 @@ export class PackageHelper { * @param packageVersion optional version of package. If none, latest will be pulled in */ public async addPackage(project: string, packageName: string, packageVersion?: string): Promise { - const addOptions: DotNetCommandOptions = { + const addOptions: ShellCommandOptions = { commandTitle: constants.addPackage, argument: this.constructAddPackageArguments(project, packageName, packageVersion) }; diff --git a/extensions/sql-database-projects/src/tools/shellExecutionHelper.ts b/extensions/sql-database-projects/src/tools/shellExecutionHelper.ts new file mode 100644 index 0000000000..5dbc1c6a45 --- /dev/null +++ b/extensions/sql-database-projects/src/tools/shellExecutionHelper.ts @@ -0,0 +1,71 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as cp from 'promisify-child-process'; +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +export interface ShellCommandOptions { + workingDirectory?: string; + additionalEnvironmentVariables?: NodeJS.ProcessEnv; + commandTitle?: string; + argument?: string; +} + +export class ShellExecutionHelper { + constructor(protected _outputChannel: vscode.OutputChannel) { + } + + /** + * spawns the shell command with arguments and redirects the error and output to ADS output channel + */ + public async runStreamedCommand(command: string, outputChannel: vscode.OutputChannel, options?: ShellCommandOptions): Promise { + const stdoutData: string[] = []; + outputChannel.appendLine(` > ${command}`); + + const spawnOptions = { + cwd: options && options.workingDirectory, + env: Object.assign({}, process.env, options && options.additionalEnvironmentVariables), + encoding: 'utf8', + maxBuffer: 10 * 1024 * 1024, // 10 Mb of output can be captured. + shell: true, + detached: false, + windowsHide: true + }; + + const child = cp.spawn(command, [], spawnOptions); + outputChannel.show(); + + // Add listeners to print stdout and stderr and exit code + void child.on('exit', (code: number | null, signal: string | null) => { + if (code !== null) { + outputChannel.appendLine(localize('sqlDatabaseProjects.RunStreamedCommand.ExitedWithCode', " >>> {0} … exited with code: {1}", command, code)); + } else { + outputChannel.appendLine(localize('sqlDatabaseProjects.RunStreamedCommand.ExitedWithSignal', " >>> {0} … exited with signal: {1}", command, signal)); + } + }); + + child.stdout!.on('data', (data: string | Buffer) => { + stdoutData.push(data.toString()); + this.outputDataChunk(data, outputChannel, localize('sqlDatabaseProjects.RunCommand.stdout', " stdout: ")); + }); + + child.stderr!.on('data', (data: string | Buffer) => { + this.outputDataChunk(data, outputChannel, localize('sqlDatabaseProjects.RunCommand.stderr', " stderr: ")); + }); + + await child; + + return stdoutData.join(''); + } + + private outputDataChunk(data: string | Buffer, outputChannel: vscode.OutputChannel, header: string): void { + data.toString().split(/\r?\n/) + .forEach(line => { + outputChannel.appendLine(header + line); + }); + } +} diff --git a/extensions/sql-database-projects/yarn.lock b/extensions/sql-database-projects/yarn.lock index 5df13e19b4..c6db4ef340 100644 --- a/extensions/sql-database-projects/yarn.lock +++ b/extensions/sql-database-projects/yarn.lock @@ -298,6 +298,11 @@ resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e" integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA== +"@types/which@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/which/-/which-2.0.1.tgz#27ecd67f915b7c3d6ba552135bb1eecd66e63501" + integrity sha512-Jjakcv8Roqtio6w1gr0D7y6twbhx6gGgFGF5BLwajPpnOIOxFkakFhCq+LmyyeAz7BX6ULrjBOxdKaCDy+4+dQ== + "@types/xml-formatter@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@types/xml-formatter/-/xml-formatter-1.1.0.tgz#f7cde70ec33d7b044029b6b6c2f6e69d270ced63" @@ -711,6 +716,11 @@ isarray@0.0.1: resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + istanbul-lib-coverage@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" @@ -821,6 +831,13 @@ lodash@^4.16.4, lodash@^4.17.13, lodash@^4.17.4: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -1036,6 +1053,13 @@ semver@^6.0.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.3.5: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + shimmer@^1.1.0, shimmer@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" @@ -1271,6 +1295,13 @@ vscodetestcover@^1.1.0: istanbul-reports "^3.0.0" mocha "^5.2.0" +which@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -1297,3 +1328,8 @@ xmldom@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.3.0.tgz#e625457f4300b5df9c2e1ecb776147ece47f3e5a" integrity sha512-z9s6k3wxE+aZHgXYxSTpGDo7BYOUfJsIRyoZiX6HTjwpwfS2wpQBQKa2fD+ShLyPkqDYo5ud7KitmLZ2Cd6r0g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==