diff --git a/CHANGELOG.md b/CHANGELOG.md index 9371ef46911..1d9052ac697 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -599,10 +599,8 @@ ## [2.28.0](https://github.com/Azure/LogicAppsUX/compare/v2.27.0...v2.28.0) (2023-06-13) - ## [2.23.0](https://github.com/Azure/LogicAppsUX/compare/v2.22.0...v2.23.0) (2023-05-31) - ### Features - **Data Mapper:** Able to select schemas from folders within 'Schemas' folder ([#2700](https://github.com/Azure/LogicAppsUX/issues/2700)) ([3999cd3](https://github.com/Azure/LogicAppsUX/commit/3999cd312d414e2a915ace6a65c0cda46d64db87)) diff --git a/apps/vs-code-designer/.eslintrc.json b/apps/vs-code-designer/.eslintrc.json index a01cef12e04..726252bf9c4 100644 --- a/apps/vs-code-designer/.eslintrc.json +++ b/apps/vs-code-designer/.eslintrc.json @@ -5,7 +5,8 @@ { "files": ["*.ts"], "rules": { - "no-param-reassign": "off" + "no-param-reassign": "off", + "react-hooks/rules-of-hooks": "off" } }, { diff --git a/apps/vs-code-designer/src/app/azuriteExtension/executeOnAzuriteExt.ts b/apps/vs-code-designer/src/app/azuriteExtension/executeOnAzuriteExt.ts new file mode 100644 index 00000000000..2ac0994e2fe --- /dev/null +++ b/apps/vs-code-designer/src/app/azuriteExtension/executeOnAzuriteExt.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { azuriteExtensionId } from '../../constants'; +import { localize } from '../../localize'; +import type { IActionContext } from '@microsoft/vscode-azext-utils'; +import { extensions } from 'vscode'; +import * as vscode from 'vscode'; + +export async function executeOnAzurite(context: IActionContext, command: string, ...args: any[]): Promise { + const azuriteExtension = extensions.getExtension(azuriteExtensionId); + + if (azuriteExtension?.isActive) { + vscode.commands.executeCommand(command, { + ...args, + }); + } else { + const message: string = localize('deactivatedAzuriteExt', 'Azurite extension is deactivated, make sure to activate it'); + await context.ui.showWarningMessage(message); + } +} diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createFunction/FunctionConfigFile.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createFunction/FunctionConfigFile.ts index 8aa276d3e42..dee87e8c4d5 100644 --- a/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createFunction/FunctionConfigFile.ts +++ b/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createFunction/FunctionConfigFile.ts @@ -107,7 +107,7 @@ export class FunctionConfigFile extends AzureWizardPromptStep { const tasksJsonPath: string = path.join(context.projectPath, '.vscode', 'Tasks.json'); const tasksJsonContent = `{ - "version": "2.0.0", - "tasks": [ - { - "label": "generateDebugSymbols", - "command": "dotnet", - "args": [ - "\${input:getDebugSymbolDll}" + "version": "2.0.0", + "tasks": [ + { + "label": "generateDebugSymbols", + "command": '\${config:azureLogicAppsStandard.dotnetBinaryPath}', + "args": [ + "\${input:getDebugSymbolDll}" + ], + "type": "process", + "problemMatcher": "$msCompile" + }, + { + "type": "shell", + "command":"\${config:azureLogicAppsStandard.funcCoreToolsBinaryPath}", + "args" : ["host", "start"], + "options": { + "env": { + "PATH": "\${config:azureLogicAppsStandard.autoRuntimeDependenciesPath}\\\\NodeJs;\${config:azureLogicAppsStandard.autoRuntimeDependenciesPath}\\\\DotNetSDK;$env:PATH" + } + }, + "problemMatcher": "$func-watch", + "isBackground": true, + "label": "func: host start", + "group": { + "kind": "build", + "isDefault": true + } + } ], - "type": "process", - "problemMatcher": "$msCompile" - }, - { - "type": "func", - "command": "host start", - "problemMatcher": "$func-watch", - "isBackground": true, - "label": "func: host start", - "group": { - "kind": "build", - "isDefault": true - } - } - ], - "inputs": [ - { - "id": "getDebugSymbolDll", - "type": "command", - "command": "azureLogicAppsStandard.getDebugSymbolDll" - } - ] - }`; + "inputs": [ + { + "id": "getDebugSymbolDll", + "type": "command", + "command": "azureLogicAppsStandard.getDebugSymbolDll" + } + ] + }`; if (await confirmOverwriteFile(context, tasksJsonPath)) { await fse.writeFile(tasksJsonPath, tasksJsonContent); diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createLogicApp/initLogicAppCodeProjectVScode/ScriptInit.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createLogicApp/initLogicAppCodeProjectVScode/ScriptInit.ts index 7bea9b0c021..5adea879ece 100644 --- a/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createLogicApp/initLogicAppCodeProjectVScode/ScriptInit.ts +++ b/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createLogicApp/initLogicAppCodeProjectVScode/ScriptInit.ts @@ -2,7 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { extInstallTaskName, func, funcWatchProblemMatcher, hostStartCommand } from '../../../../../../constants'; +import { binariesExist } from '../../../../../../app/utils/binaries'; +import { extInstallTaskName, func, funcDependencyName, funcWatchProblemMatcher, hostStartCommand } from '../../../../../../constants'; import { getLocalFuncCoreToolsVersion } from '../../../../../utils/funcCoreTools/funcVersion'; import { InitCodeProject } from './InitCodeProject'; import type { IProjectWizardContext } from '@microsoft/vscode-extension'; @@ -19,10 +20,23 @@ export class ScriptInit extends InitCodeProject { protected useFuncExtensionsInstall = false; protected getTasks(): TaskDefinition[] { + const funcBinariesExist = binariesExist(funcDependencyName); + const binariesOptions = funcBinariesExist + ? { + options: { + env: { + PATH: '${config:azureLogicAppsStandard.autoRuntimeDependenciesPath}\\NodeJs;${config:azureLogicAppsStandard.autoRuntimeDependenciesPath}\\DotNetSDK;$env:PATH', + }, + }, + } + : {}; return [ { - type: func, - command: hostStartCommand, + label: 'func: host start', + type: funcBinariesExist ? 'shell' : func, + command: funcBinariesExist ? '${config:azureLogicAppsStandard.funcCoreToolsBinaryPath}' : hostStartCommand, + args: funcBinariesExist ? ['host', 'start'] : undefined, + ...binariesOptions, problemMatcher: funcWatchProblemMatcher, dependsOn: this.useFuncExtensionsInstall ? extInstallTaskName : undefined, isBackground: true, diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createLogicApp/initLogicAppCodeProjectVScode/WorkflowCode.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createLogicApp/initLogicAppCodeProjectVScode/WorkflowCode.ts index 4f06079ffae..985540ea6e3 100644 --- a/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createLogicApp/initLogicAppCodeProjectVScode/WorkflowCode.ts +++ b/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createLogicApp/initLogicAppCodeProjectVScode/WorkflowCode.ts @@ -2,7 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { extensionCommand, func, funcWatchProblemMatcher, hostStartCommand } from '../../../../../../constants'; +import { binariesExist } from '../../../../../../app/utils/binaries'; +import { extensionCommand, func, funcDependencyName, funcWatchProblemMatcher, hostStartCommand } from '../../../../../../constants'; import { ScriptInit } from './ScriptInit'; import type { IProjectWizardContext, ITaskInputs, ISettingToAdd } from '@microsoft/vscode-extension'; import type { TaskDefinition } from 'vscode'; @@ -13,17 +14,29 @@ export class WorkflowInitCodeProject extends ScriptInit { } protected getTasks(): TaskDefinition[] { + const funcBinariesExist = binariesExist(funcDependencyName); + const binariesOptions = funcBinariesExist + ? { + options: { + env: { + PATH: '${config:azureLogicAppsStandard.autoRuntimeDependenciesPath}\\NodeJs;${config:azureLogicAppsStandard.autoRuntimeDependenciesPath}\\DotNetSDK;$env:PATH', + }, + }, + } + : {}; return [ { label: 'generateDebugSymbols', - command: 'dotnet', + command: '${config:azureLogicAppsStandard.dotnetBinaryPath}', args: ['${input:getDebugSymbolDll}'], type: 'process', problemMatcher: '$msCompile', }, { - type: func, - command: hostStartCommand, + type: funcBinariesExist ? 'shell' : func, + command: funcBinariesExist ? '${config:azureLogicAppsStandard.funcCoreToolsBinaryPath}' : hostStartCommand, + args: funcBinariesExist ? ['host', 'start'] : undefined, + ...binariesOptions, problemMatcher: funcWatchProblemMatcher, isBackground: true, label: 'func: host start', diff --git a/apps/vs-code-designer/src/app/commands/dotnet/installDotNet.ts b/apps/vs-code-designer/src/app/commands/dotnet/installDotNet.ts new file mode 100644 index 00000000000..c5121393802 --- /dev/null +++ b/apps/vs-code-designer/src/app/commands/dotnet/installDotNet.ts @@ -0,0 +1,21 @@ +/*------------------p--------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { autoRuntimeDependenciesPathSettingKey, dotnetDependencyName } from '../../../constants'; +import { ext } from '../../../extensionVariables'; +import { downloadAndExtractBinaries, getDotNetBinariesReleaseUrl } from '../../utils/binaries'; +import { getGlobalSetting } from '../../utils/vsCodeConfig/settings'; +import type { IActionContext } from '@microsoft/vscode-azext-utils'; + +export async function installDotNet(context: IActionContext, majorVersion?: string): Promise { + ext.outputChannel.show(); + context.telemetry.properties.majorVersion = majorVersion; + const targetDirectory = getGlobalSetting(autoRuntimeDependenciesPathSettingKey); + + context.telemetry.properties.lastStep = 'getDotNetBinariesReleaseUrl'; + const scriptUrl = getDotNetBinariesReleaseUrl(); + + context.telemetry.properties.lastStep = 'downloadAndExtractBinaries'; + await downloadAndExtractBinaries(scriptUrl, targetDirectory, dotnetDependencyName, majorVersion); +} diff --git a/apps/vs-code-designer/src/app/commands/dotnet/validateDotNetInstalled.ts b/apps/vs-code-designer/src/app/commands/dotnet/validateDotNetInstalled.ts new file mode 100644 index 00000000000..2c7677b5efd --- /dev/null +++ b/apps/vs-code-designer/src/app/commands/dotnet/validateDotNetInstalled.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { validateDotNetSDKSetting } from '../../../constants'; +import { localize } from '../../../localize'; +import { getDotNetCommand } from '../../utils/dotnet/dotnet'; +import { executeCommand } from '../../utils/funcCoreTools/cpUtils'; +import { getWorkspaceSetting } from '../../utils/vsCodeConfig/settings'; +import { installDotNet } from './installDotNet'; +import { callWithTelemetryAndErrorHandling, DialogResponses, openUrl } from '@microsoft/vscode-azext-utils'; +import type { IActionContext } from '@microsoft/vscode-azext-utils'; +import type { MessageItem } from 'vscode'; + +/** + * Checks if dotnet 6 is installed, and installs it if needed. + * @param {IActionContext} context - Workflow file path. + * @param {string} message - Message for warning. + * @param {string} fsPath - Workspace file system path. + * @returns {Promise} Returns true if it is installed or was sucessfully installed, otherwise returns false. + */ +export async function validateDotNetIsInstalled(context: IActionContext, message: string, fsPath: string): Promise { + let input: MessageItem | undefined; + let installed = false; + const install: MessageItem = { title: localize('install', 'Install') }; + + await callWithTelemetryAndErrorHandling('azureLogicAppsStandard.validateDotNetIsInstalled', async (innerContext: IActionContext) => { + innerContext.errorHandling.suppressDisplay = true; + + if (!getWorkspaceSetting(validateDotNetSDKSetting, fsPath)) { + innerContext.telemetry.properties.validateDotNet = 'false'; + installed = true; + } else if (await isDotNetInstalled()) { + installed = true; + } else { + const items: MessageItem[] = [install, DialogResponses.learnMore]; + input = await innerContext.ui.showWarningMessage(message, { modal: true }, ...items); + innerContext.telemetry.properties.dialogResult = input.title; + + if (input === install) { + await installDotNet(innerContext); + installed = true; + } else if (input === DialogResponses.learnMore) { + await openUrl('https://dotnet.microsoft.com/download/dotnet/6.0'); + } + } + }); + + // validate that DotNet was installed only if user confirmed + if (input === install && !installed) { + if ( + (await context.ui.showWarningMessage( + localize('failedInstallDotNet', 'The .NET SDK installation failed. Please manually install instead.'), + DialogResponses.learnMore + )) === DialogResponses.learnMore + ) { + await openUrl('https://dotnet.microsoft.com/download/dotnet/6.0'); + } + } + + return installed; +} + +/** + * Check is dotnet is installed. + * @returns {Promise} Returns true if installed, otherwise returns false. + */ +export async function isDotNetInstalled(): Promise { + try { + await executeCommand(undefined, undefined, getDotNetCommand(), '--version'); + return true; + } catch (error) { + return false; + } +} diff --git a/apps/vs-code-designer/src/app/commands/dotnet/validateDotNetIsLatest.ts b/apps/vs-code-designer/src/app/commands/dotnet/validateDotNetIsLatest.ts new file mode 100644 index 00000000000..7a0fcf7bc17 --- /dev/null +++ b/apps/vs-code-designer/src/app/commands/dotnet/validateDotNetIsLatest.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { dotnetDependencyName } from '../../../constants'; +import { localize } from '../../../localize'; +import { binariesExist, getLatestDotNetVersion } from '../../utils/binaries'; +import { getDotNetCommand, getLocalDotNetVersionFromBinaries } from '../../utils/dotnet/dotnet'; +import { getWorkspaceSetting, updateGlobalSetting } from '../../utils/vsCodeConfig/settings'; +import { installDotNet } from './installDotNet'; +import { callWithTelemetryAndErrorHandling, DialogResponses, openUrl } from '@microsoft/vscode-azext-utils'; +import type { IActionContext } from '@microsoft/vscode-azext-utils'; +import * as semver from 'semver'; +import type { MessageItem } from 'vscode'; + +export async function validateDotNetIsLatest(majorVersion?: string): Promise { + await callWithTelemetryAndErrorHandling('azureLogicAppsStandard.validateDotNetIsLatest', async (context: IActionContext) => { + context.errorHandling.suppressDisplay = true; + context.telemetry.properties.isActivationEvent = 'true'; + + const showDotNetWarningKey = 'showDotNetWarning'; + const showDotNetWarning = !!getWorkspaceSetting(showDotNetWarningKey); + const binaries = binariesExist(dotnetDependencyName); + context.telemetry.properties.binariesExist = `${binaries}`; + + if (!binaries) { + await installDotNet(context, majorVersion); + context.telemetry.properties.binaryCommand = `${getDotNetCommand()}`; + } else if (showDotNetWarning) { + context.telemetry.properties.binaryCommand = `${getDotNetCommand()}`; + const localVersion: string | null = await getLocalDotNetVersionFromBinaries(); + context.telemetry.properties.localVersion = localVersion; + const newestVersion: string | undefined = await getLatestDotNetVersion(context, majorVersion); + if (semver.major(newestVersion) === semver.major(localVersion) && semver.gt(newestVersion, localVersion)) { + context.telemetry.properties.outOfDateDotNet = 'true'; + const message: string = localize( + 'outdatedDotNetRuntime', + 'Update your local .NET SDK version ({0}) to the latest version ({1}) for the best experience.', + localVersion, + newestVersion + ); + const update: MessageItem = { title: 'Update' }; + let result: MessageItem; + do { + result = + newestVersion !== undefined + ? await context.ui.showWarningMessage(message, update, DialogResponses.learnMore, DialogResponses.dontWarnAgain) + : await context.ui.showWarningMessage(message, DialogResponses.learnMore, DialogResponses.dontWarnAgain); + if (result === DialogResponses.learnMore) { + await openUrl('https://dotnet.microsoft.com/en-us/download/dotnet/6.0'); + } else if (result === update) { + await installDotNet(context, majorVersion); + } else if (result === DialogResponses.dontWarnAgain) { + await updateGlobalSetting(showDotNetWarningKey, false); + } + } while (result === DialogResponses.learnMore); + } + } + }); +} diff --git a/apps/vs-code-designer/src/app/commands/funcCoreTools/installFuncCoreTools.ts b/apps/vs-code-designer/src/app/commands/funcCoreTools/installFuncCoreTools.ts index 14f8408484b..0946e16b886 100644 --- a/apps/vs-code-designer/src/app/commands/funcCoreTools/installFuncCoreTools.ts +++ b/apps/vs-code-designer/src/app/commands/funcCoreTools/installFuncCoreTools.ts @@ -2,17 +2,49 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { funcPackageName, PackageManager } from '../../../constants'; +import { PackageManager, Platform, autoRuntimeDependenciesPathSettingKey, funcDependencyName, funcPackageName } from '../../../constants'; import { ext } from '../../../extensionVariables'; -import { localize } from '../../../localize'; +import { + downloadAndExtractBinaries, + getCpuArchitecture, + getFunctionCoreToolsBinariesReleaseUrl, + getLatestFunctionCoreToolsVersion, +} from '../../utils/binaries'; import { executeCommand } from '../../utils/funcCoreTools/cpUtils'; import { getBrewPackageName } from '../../utils/funcCoreTools/getBrewPackageName'; import { getNpmDistTag } from '../../utils/funcCoreTools/getNpmDistTag'; -import { promptForFuncVersion } from '../../utils/vsCodeConfig/settings'; +import { getGlobalSetting, promptForFuncVersion } from '../../utils/vsCodeConfig/settings'; import type { IActionContext } from '@microsoft/vscode-azext-utils'; import type { FuncVersion, INpmDistTag } from '@microsoft/vscode-extension'; +import { localize } from 'vscode-nls'; -export async function installFuncCoreTools( +export async function installFuncCoreToolsBinaries(context: IActionContext, majorVersion?: string): Promise { + ext.outputChannel.show(); + const arch = getCpuArchitecture(); + const targetDirectory = getGlobalSetting(autoRuntimeDependenciesPathSettingKey); + context.telemetry.properties.lastStep = 'getLatestFunctionCoreToolsVersion'; + const version = await getLatestFunctionCoreToolsVersion(context, majorVersion); + let azureFunctionCoreToolsReleasesUrl; + + context.telemetry.properties.lastStep = 'getFunctionCoreToolsBinariesReleaseUrl'; + switch (process.platform) { + case Platform.windows: + azureFunctionCoreToolsReleasesUrl = getFunctionCoreToolsBinariesReleaseUrl(version, 'win', arch); + break; + + case Platform.linux: + azureFunctionCoreToolsReleasesUrl = getFunctionCoreToolsBinariesReleaseUrl(version, 'linux', arch); + break; + + case Platform.mac: + azureFunctionCoreToolsReleasesUrl = getFunctionCoreToolsBinariesReleaseUrl(version, 'osx', arch); + break; + } + context.telemetry.properties.lastStep = 'downloadAndExtractBinaries'; + await downloadAndExtractBinaries(azureFunctionCoreToolsReleasesUrl, targetDirectory, funcDependencyName); +} + +export async function installFuncCoreToolsSystem( context: IActionContext, packageManagers: PackageManager[], version?: FuncVersion diff --git a/apps/vs-code-designer/src/app/commands/funcCoreTools/validateFuncCoreToolsInstalled.ts b/apps/vs-code-designer/src/app/commands/funcCoreTools/validateFuncCoreToolsInstalled.ts index 4ca7ebaa554..9577278e006 100644 --- a/apps/vs-code-designer/src/app/commands/funcCoreTools/validateFuncCoreToolsInstalled.ts +++ b/apps/vs-code-designer/src/app/commands/funcCoreTools/validateFuncCoreToolsInstalled.ts @@ -2,15 +2,14 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { PackageManager } from '../../../constants'; -import { validateFuncCoreToolsSetting, funcVersionSetting } from '../../../constants'; -import { ext } from '../../../extensionVariables'; +import { type PackageManager, funcVersionSetting, validateFuncCoreToolsSetting } from '../../../constants'; import { localize } from '../../../localize'; +import { useBinariesDependencies } from '../../utils/binaries'; import { executeCommand } from '../../utils/funcCoreTools/cpUtils'; -import { tryParseFuncVersion } from '../../utils/funcCoreTools/funcVersion'; +import { getFunctionsCommand, tryParseFuncVersion } from '../../utils/funcCoreTools/funcVersion'; import { getFuncPackageManagers } from '../../utils/funcCoreTools/getFuncPackageManagers'; import { getWorkspaceSetting } from '../../utils/vsCodeConfig/settings'; -import { installFuncCoreTools } from './installFuncCoreTools'; +import { installFuncCoreToolsBinaries, installFuncCoreToolsSystem } from './installFuncCoreTools'; import { callWithTelemetryAndErrorHandling, DialogResponses, openUrl } from '@microsoft/vscode-azext-utils'; import type { IActionContext } from '@microsoft/vscode-azext-utils'; import type { FuncVersion } from '@microsoft/vscode-extension'; @@ -37,24 +36,10 @@ export async function validateFuncCoreToolsInstalled(context: IActionContext, me } else if (await isFuncToolsInstalled()) { installed = true; } else { - const items: MessageItem[] = []; - const packageManagers: PackageManager[] = await getFuncPackageManagers(false /* isFuncInstalled */); - if (packageManagers.length > 0) { - items.push(install); + if (useBinariesDependencies()) { + installed = await validateFuncCoreToolsInstalledBinaries(innerContext, message, install, input, installed); } else { - items.push(DialogResponses.learnMore); - } - - input = await innerContext.ui.showWarningMessage(message, { modal: true }, ...items); - - innerContext.telemetry.properties.dialogResult = input.title; - - if (input === install) { - const version: FuncVersion | undefined = tryParseFuncVersion(getWorkspaceSetting(funcVersionSetting, fsPath)); - await installFuncCoreTools(innerContext, packageManagers, version); - installed = true; - } else if (input === DialogResponses.learnMore) { - await openUrl('https://aka.ms/Dqur4e'); + installed = await validateFuncCoreToolsInstalledSystem(innerContext, message, install, input, installed, fsPath); } } }); @@ -74,13 +59,65 @@ export async function validateFuncCoreToolsInstalled(context: IActionContext, me return installed; } +const validateFuncCoreToolsInstalledBinaries = async ( + innerContext: IActionContext, + message: string, + install: MessageItem, + input: MessageItem | undefined, + installed: boolean +) => { + const items: MessageItem[] = [install, DialogResponses.learnMore]; + input = await innerContext.ui.showWarningMessage(message, { modal: true }, ...items); + innerContext.telemetry.properties.dialogResult = input.title; + + if (input === install) { + await installFuncCoreToolsBinaries(innerContext); + installed = true; + } else if (input === DialogResponses.learnMore) { + await openUrl('https://aka.ms/Dqur4e'); + } + + return installed; +}; + +const validateFuncCoreToolsInstalledSystem = async ( + innerContext: IActionContext, + message: string, + install: MessageItem, + input: MessageItem | undefined, + installed: boolean, + fsPath: string +) => { + const items: MessageItem[] = []; + const packageManagers: PackageManager[] = await getFuncPackageManagers(false /* isFuncInstalled */); + if (packageManagers.length > 0) { + items.push(install); + } else { + items.push(DialogResponses.learnMore); + } + + input = await innerContext.ui.showWarningMessage(message, { modal: true }, ...items); + + innerContext.telemetry.properties.dialogResult = input.title; + + if (input === install) { + const version: FuncVersion | undefined = tryParseFuncVersion(getWorkspaceSetting(funcVersionSetting, fsPath)); + await installFuncCoreToolsSystem(innerContext, packageManagers, version); + installed = true; + } else if (input === DialogResponses.learnMore) { + await openUrl('https://aka.ms/Dqur4e'); + } + return installed; +}; + /** * Check is functions core tools is installed. * @returns {Promise} Returns true if installed, otherwise returns false. */ export async function isFuncToolsInstalled(): Promise { + const funcCommand = getFunctionsCommand(); try { - await executeCommand(undefined, undefined, ext.funcCliPath, '--version'); + await executeCommand(undefined, undefined, funcCommand, '--version'); return true; } catch (error) { return false; diff --git a/apps/vs-code-designer/src/app/commands/funcCoreTools/validateFuncCoreToolsIsLatest.ts b/apps/vs-code-designer/src/app/commands/funcCoreTools/validateFuncCoreToolsIsLatest.ts index 9a872ebeea5..bf765d83511 100644 --- a/apps/vs-code-designer/src/app/commands/funcCoreTools/validateFuncCoreToolsIsLatest.ts +++ b/apps/vs-code-designer/src/app/commands/funcCoreTools/validateFuncCoreToolsIsLatest.ts @@ -2,15 +2,17 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { PackageManager } from '../../../constants'; +import { PackageManager, funcDependencyName } from '../../../constants'; import { localize } from '../../../localize'; import { executeOnFunctions } from '../../functionsExtension/executeOnFunctionsExt'; -import { getLocalFuncCoreToolsVersion, tryParseFuncVersion } from '../../utils/funcCoreTools/funcVersion'; +import { binariesExist, getLatestFunctionCoreToolsVersion, useBinariesDependencies } from '../../utils/binaries'; +import { getFunctionsCommand, getLocalFuncCoreToolsVersion, tryParseFuncVersion } from '../../utils/funcCoreTools/funcVersion'; import { getBrewPackageName } from '../../utils/funcCoreTools/getBrewPackageName'; import { getFuncPackageManagers } from '../../utils/funcCoreTools/getFuncPackageManagers'; import { getNpmDistTag } from '../../utils/funcCoreTools/getNpmDistTag'; import { sendRequestWithExtTimeout } from '../../utils/requestUtils'; import { getWorkspaceSetting, updateGlobalSetting } from '../../utils/vsCodeConfig/settings'; +import { installFuncCoreToolsBinaries } from './installFuncCoreTools'; import { uninstallFuncCoreTools } from './uninstallFuncCoreTools'; import { updateFuncCoreTools } from './updateFuncCoreTools'; import { HTTP_METHODS } from '@microsoft/utils-logic-apps'; @@ -20,7 +22,62 @@ import type { FuncVersion } from '@microsoft/vscode-extension'; import * as semver from 'semver'; import type { MessageItem } from 'vscode'; -export async function validateFuncCoreToolsIsLatest(): Promise { +export async function validateFuncCoreToolsIsLatest(majorVersion?: string): Promise { + if (useBinariesDependencies()) { + await validateFuncCoreToolsIsLatestBinaries(majorVersion); + } else { + await validateFuncCoreToolsIsLatestSystem(); + } +} + +export async function validateFuncCoreToolsIsLatestBinaries(majorVersion?: string): Promise { + await callWithTelemetryAndErrorHandling('azureLogicAppsStandard.validateFuncCoreToolsIsLatest', async (context: IActionContext) => { + context.errorHandling.suppressDisplay = true; + context.telemetry.properties.isActivationEvent = 'true'; + + const showCoreToolsWarningKey = 'showCoreToolsWarning'; + const showCoreToolsWarning = !!getWorkspaceSetting(showCoreToolsWarningKey); + + const binaries = binariesExist(funcDependencyName); + context.telemetry.properties.binariesExist = `${binaries}`; + + if (!binaries) { + await installFuncCoreToolsBinaries(context, majorVersion); + context.telemetry.properties.binaryCommand = `${getFunctionsCommand()}`; + } else if (showCoreToolsWarning) { + context.telemetry.properties.binaryCommand = `${getFunctionsCommand()}`; + const localVersion: string | null = await getLocalFuncCoreToolsVersion(); + context.telemetry.properties.localVersion = localVersion; + const newestVersion: string | undefined = await getLatestFunctionCoreToolsVersion(context, majorVersion); + if (semver.major(newestVersion) === semver.major(localVersion) && semver.gt(newestVersion, localVersion)) { + context.telemetry.properties.outOfDateFunc = 'true'; + const message: string = localize( + 'outdatedFunctionRuntime', + 'Update your local Azure Functions Core Tools version ({0}) to the latest version ({1}) for the best experience.', + localVersion, + newestVersion + ); + const update: MessageItem = { title: localize('update', 'Update') }; + let result: MessageItem; + do { + result = + newestVersion !== undefined + ? await context.ui.showWarningMessage(message, update, DialogResponses.learnMore, DialogResponses.dontWarnAgain) + : await context.ui.showWarningMessage(message, DialogResponses.learnMore, DialogResponses.dontWarnAgain); + if (result === DialogResponses.learnMore) { + await openUrl('https://aka.ms/azFuncOutdated'); + } else if (result === update) { + await installFuncCoreToolsBinaries(context, majorVersion); + } else if (result === DialogResponses.dontWarnAgain) { + await updateGlobalSetting(showCoreToolsWarningKey, false); + } + } while (result === DialogResponses.learnMore); + } + } + }); +} + +export async function validateFuncCoreToolsIsLatestSystem(): Promise { await callWithTelemetryAndErrorHandling('azureLogicAppsStandard.validateFuncCoreToolsIsLatest', async (context: IActionContext) => { context.errorHandling.suppressDisplay = true; context.telemetry.properties.isActivationEvent = 'true'; @@ -84,7 +141,7 @@ export async function validateFuncCoreToolsIsLatest(): Promise { newestVersion ); - const update: MessageItem = { title: 'Update' }; + const update: MessageItem = { title: localize('update', 'Update') }; let result: MessageItem; do { diff --git a/apps/vs-code-designer/src/app/commands/initProjectForVSCode/DotnetInitVSCodeStep.ts b/apps/vs-code-designer/src/app/commands/initProjectForVSCode/DotnetInitVSCodeStep.ts index 834a24d78ce..90c2eb3ec78 100644 --- a/apps/vs-code-designer/src/app/commands/initProjectForVSCode/DotnetInitVSCodeStep.ts +++ b/apps/vs-code-designer/src/app/commands/initProjectForVSCode/DotnetInitVSCodeStep.ts @@ -3,14 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { - dotnetExtensionId, dotnetPublishTaskLabel, + funcDependencyName, + dotnetExtensionId, func, funcWatchProblemMatcher, hostStartCommand, show64BitWarningSetting, } from '../../../constants'; import { localize } from '../../../localize'; +import { binariesExist } from '../../utils/binaries'; import { getProjFiles, getTargetFramework, getDotnetDebugSubpath, tryGetFuncVersion } from '../../utils/dotnet/dotnet'; import type { ProjectFile } from '../../utils/dotnet/dotnet'; import { tryParseFuncVersion } from '../../utils/funcCoreTools/funcVersion'; @@ -106,18 +108,28 @@ export class DotnetInitVSCodeStep extends InitVSCodeStepBase { protected getTasks(): TaskDefinition[] { const commonArgs: string[] = ['/property:GenerateFullPaths=true', '/consoleloggerparameters:NoSummary']; const releaseArgs: string[] = ['--configuration', 'Release']; - + const funcBinariesExist = binariesExist(funcDependencyName); + const binariesOptions = funcBinariesExist + ? { + options: { + cwd: this.debugSubpath, + env: { + PATH: '${config:azureLogicAppsStandard.autoRuntimeDependenciesPath}\\NodeJs;${config:azureLogicAppsStandard.autoRuntimeDependenciesPath}\\DotNetSDK;$env:PATH', + }, + }, + } + : {}; return [ { label: 'clean', - command: 'dotnet', + command: '${config:azureLogicAppsStandard.dotnetBinaryPath}', args: ['clean', ...commonArgs], type: 'process', problemMatcher: '$msCompile', }, { label: 'build', - command: 'dotnet', + command: '${config:azureLogicAppsStandard.dotnetBinaryPath}', args: ['build', ...commonArgs], type: 'process', dependsOn: 'clean', @@ -129,26 +141,26 @@ export class DotnetInitVSCodeStep extends InitVSCodeStepBase { }, { label: 'clean release', - command: 'dotnet', + command: '${config:azureLogicAppsStandard.dotnetBinaryPath}', args: ['clean', ...releaseArgs, ...commonArgs], type: 'process', problemMatcher: '$msCompile', }, { label: dotnetPublishTaskLabel, - command: 'dotnet', + command: '${config:azureLogicAppsStandard.dotnetBinaryPath}', args: ['publish', ...releaseArgs, ...commonArgs], type: 'process', dependsOn: 'clean release', problemMatcher: '$msCompile', }, { - type: func, + label: 'func: host start', + type: funcBinariesExist ? 'shell' : func, dependsOn: 'build', - options: { - cwd: this.debugSubpath, - }, - command: hostStartCommand, + ...binariesOptions, + command: funcBinariesExist ? '${config:azureLogicAppsStandard.funcCoreToolsBinaryPath}' : hostStartCommand, + args: funcBinariesExist ? ['host', 'start'] : undefined, isBackground: true, problemMatcher: funcWatchProblemMatcher, }, diff --git a/apps/vs-code-designer/src/app/commands/initProjectForVSCode/ScriptInitVSCodeStep.ts b/apps/vs-code-designer/src/app/commands/initProjectForVSCode/ScriptInitVSCodeStep.ts index 336fde8ffee..20e783eafd0 100644 --- a/apps/vs-code-designer/src/app/commands/initProjectForVSCode/ScriptInitVSCodeStep.ts +++ b/apps/vs-code-designer/src/app/commands/initProjectForVSCode/ScriptInitVSCodeStep.ts @@ -2,7 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { extInstallTaskName, func, funcWatchProblemMatcher, hostStartCommand } from '../../../constants'; +import { extInstallTaskName, func, funcDependencyName, funcWatchProblemMatcher, hostStartCommand } from '../../../constants'; +import { binariesExist } from '../../utils/binaries'; import { getLocalFuncCoreToolsVersion } from '../../utils/funcCoreTools/funcVersion'; import { InitVSCodeStepBase } from './InitVSCodeStepBase'; import type { IProjectWizardContext } from '@microsoft/vscode-extension'; @@ -19,10 +20,23 @@ export class ScriptInitVSCodeStep extends InitVSCodeStepBase { protected useFuncExtensionsInstall = false; protected getTasks(): TaskDefinition[] { + const funcBinariesExist = binariesExist(funcDependencyName); + const binariesOptions = funcBinariesExist + ? { + options: { + env: { + PATH: '${config:azureLogicAppsStandard.autoRuntimeDependenciesPath}\\NodeJs;${config:azureLogicAppsStandard.autoRuntimeDependenciesPath}\\DotNetSDK;$env:PATH', + }, + }, + } + : {}; return [ { - type: func, - command: hostStartCommand, + label: 'func: host start', + type: funcBinariesExist ? 'shell' : func, + command: funcBinariesExist ? '${config:azureLogicAppsStandard.funcCoreToolsBinaryPath}' : hostStartCommand, + args: funcBinariesExist ? ['host', 'start'] : undefined, + ...binariesOptions, problemMatcher: funcWatchProblemMatcher, dependsOn: this.useFuncExtensionsInstall ? extInstallTaskName : undefined, isBackground: true, diff --git a/apps/vs-code-designer/src/app/commands/initProjectForVSCode/WorkflowInitVSCodeStep.ts b/apps/vs-code-designer/src/app/commands/initProjectForVSCode/WorkflowInitVSCodeStep.ts index 6f438e4c569..e5cb2d84443 100644 --- a/apps/vs-code-designer/src/app/commands/initProjectForVSCode/WorkflowInitVSCodeStep.ts +++ b/apps/vs-code-designer/src/app/commands/initProjectForVSCode/WorkflowInitVSCodeStep.ts @@ -2,7 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { extensionCommand, func, funcWatchProblemMatcher, hostStartCommand } from '../../../constants'; +import { binariesExist } from '../../../app/utils/binaries'; +import { extensionCommand, func, funcDependencyName, funcWatchProblemMatcher, hostStartCommand } from '../../../constants'; import { ScriptInitVSCodeStep } from './ScriptInitVSCodeStep'; import type { IProjectWizardContext, ITaskInputs, ISettingToAdd } from '@microsoft/vscode-extension'; import type { TaskDefinition } from 'vscode'; @@ -13,19 +14,36 @@ export class WorkflowInitVSCodeStep extends ScriptInitVSCodeStep { } protected getTasks(): TaskDefinition[] { + const funcBinariesExist = binariesExist(funcDependencyName); + const binariesOptions = funcBinariesExist + ? { + options: { + env: { + PATH: '${config:azureLogicAppsStandard.autoRuntimeDependenciesPath}\\NodeJs;${config:azureLogicAppsStandard.autoRuntimeDependenciesPath}\\DotNetSDK;$env:PATH', + }, + }, + } + : {}; return [ { label: 'generateDebugSymbols', - command: 'dotnet', + command: '${config:azureLogicAppsStandard.dotnetBinaryPath}', args: ['${input:getDebugSymbolDll}'], type: 'process', problemMatcher: '$msCompile', }, { - type: func, - command: hostStartCommand, + type: funcBinariesExist ? 'shell' : func, + command: funcBinariesExist ? '${config:azureLogicAppsStandard.funcCoreToolsBinaryPath}' : hostStartCommand, + args: funcBinariesExist ? ['host', 'start'] : undefined, + ...binariesOptions, problemMatcher: funcWatchProblemMatcher, isBackground: true, + label: 'func: host start', + group: { + kind: 'build', + isDefault: true, + }, }, ]; } diff --git a/apps/vs-code-designer/src/app/commands/nodeJs/installNodeJs.ts b/apps/vs-code-designer/src/app/commands/nodeJs/installNodeJs.ts new file mode 100644 index 00000000000..1006a4e7fe4 --- /dev/null +++ b/apps/vs-code-designer/src/app/commands/nodeJs/installNodeJs.ts @@ -0,0 +1,36 @@ +/*------------------p--------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Platform, autoRuntimeDependenciesPathSettingKey, nodeJsDependencyName } from '../../../constants'; +import { ext } from '../../../extensionVariables'; +import { downloadAndExtractBinaries, getCpuArchitecture, getLatestNodeJsVersion, getNodeJsBinariesReleaseUrl } from '../../utils/binaries'; +import { getGlobalSetting } from '../../utils/vsCodeConfig/settings'; +import type { IActionContext } from '@microsoft/vscode-azext-utils'; + +export async function installNodeJs(context: IActionContext, majorVersion?: string): Promise { + ext.outputChannel.show(); + const arch = getCpuArchitecture(); + const targetDirectory = getGlobalSetting(autoRuntimeDependenciesPathSettingKey); + context.telemetry.properties.lastStep = 'getLatestNodeJsVersion'; + const version = await getLatestNodeJsVersion(context, majorVersion); + let nodeJsReleaseUrl; + + context.telemetry.properties.lastStep = 'getNodeJsBinariesReleaseUrl'; + switch (process.platform) { + case Platform.windows: + nodeJsReleaseUrl = getNodeJsBinariesReleaseUrl(version, 'win', arch); + break; + + case Platform.linux: + nodeJsReleaseUrl = getNodeJsBinariesReleaseUrl(version, 'linux', arch); + break; + + case Platform.mac: + nodeJsReleaseUrl = getNodeJsBinariesReleaseUrl(version, 'darwin', arch); + break; + } + + context.telemetry.properties.lastStep = 'downloadAndExtractBinaries'; + await downloadAndExtractBinaries(nodeJsReleaseUrl, targetDirectory, nodeJsDependencyName); +} diff --git a/apps/vs-code-designer/src/app/commands/nodeJs/validateNodeJsInstalled.ts b/apps/vs-code-designer/src/app/commands/nodeJs/validateNodeJsInstalled.ts new file mode 100644 index 00000000000..f32b05431ad --- /dev/null +++ b/apps/vs-code-designer/src/app/commands/nodeJs/validateNodeJsInstalled.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { validateNodeJsSetting } from '../../../constants'; +import { localize } from '../../../localize'; +import { executeCommand } from '../../utils/funcCoreTools/cpUtils'; +import { getNodeJsCommand } from '../../utils/nodeJs/nodeJsVersion'; +import { getWorkspaceSetting } from '../../utils/vsCodeConfig/settings'; +import { installNodeJs } from './installNodeJs'; +import { callWithTelemetryAndErrorHandling, DialogResponses, openUrl } from '@microsoft/vscode-azext-utils'; +import type { IActionContext } from '@microsoft/vscode-azext-utils'; +import type { MessageItem } from 'vscode'; + +/** + * Checks if node is installed, and installs it if needed. + * @param {IActionContext} context - Workflow file path. + * @param {string} message - Message for warning. + * @param {string} fsPath - Workspace file system path. + * @returns {Promise} Returns true if it is installed or was sucessfully installed, otherwise returns false. + */ +export async function validateNodeJsInstalled(context: IActionContext, message: string, fsPath: string): Promise { + let input: MessageItem | undefined; + let installed = false; + const install: MessageItem = { title: localize('install', 'Install') }; + + await callWithTelemetryAndErrorHandling('azureLogicAppsStandard.validateNodeJsIsInstalled', async (innerContext: IActionContext) => { + innerContext.errorHandling.suppressDisplay = true; + + if (!getWorkspaceSetting(validateNodeJsSetting, fsPath)) { + innerContext.telemetry.properties.validateDotNet = 'false'; + installed = true; + } else if (await isNodeJsInstalled()) { + installed = true; + } else { + const items: MessageItem[] = [install, DialogResponses.learnMore]; + input = await innerContext.ui.showWarningMessage(message, { modal: true }, ...items); + innerContext.telemetry.properties.dialogResult = input.title; + + if (input === install) { + await installNodeJs(innerContext); + installed = true; + } else if (input === DialogResponses.learnMore) { + await openUrl('https://nodejs.org/en/download'); + } + } + }); + + // validate that DotNet was installed only if user confirmed + if (input === install && !installed) { + if ( + (await context.ui.showWarningMessage( + localize('failedInstallDotNet', 'The Node JS installation failed. Please manually install instead.'), + DialogResponses.learnMore + )) === DialogResponses.learnMore + ) { + await openUrl('https://nodejs.org/en/download'); + } + } + + return installed; +} + +/** + * Check is dotnet is installed. + * @returns {Promise} Returns true if installed, otherwise returns false. + */ +export async function isNodeJsInstalled(): Promise { + try { + await executeCommand(undefined, undefined, getNodeJsCommand(), '--version'); + return true; + } catch (error) { + return false; + } +} diff --git a/apps/vs-code-designer/src/app/commands/nodeJs/validateNodeJsIsLatest.ts b/apps/vs-code-designer/src/app/commands/nodeJs/validateNodeJsIsLatest.ts new file mode 100644 index 00000000000..047c4be0eb6 --- /dev/null +++ b/apps/vs-code-designer/src/app/commands/nodeJs/validateNodeJsIsLatest.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { nodeJsDependencyName } from '../../../constants'; +import { localize } from '../../../localize'; +import { binariesExist, getLatestNodeJsVersion } from '../../utils/binaries'; +import { getLocalNodeJsVersion, getNodeJsCommand } from '../../utils/nodeJs/nodeJsVersion'; +import { getWorkspaceSetting, updateGlobalSetting } from '../../utils/vsCodeConfig/settings'; +import { installNodeJs } from './installNodeJs'; +import { callWithTelemetryAndErrorHandling, DialogResponses, openUrl } from '@microsoft/vscode-azext-utils'; +import type { IActionContext } from '@microsoft/vscode-azext-utils'; +import * as semver from 'semver'; +import type { MessageItem } from 'vscode'; + +export async function validateNodeJsIsLatest(majorVersion?: string): Promise { + await callWithTelemetryAndErrorHandling('azureLogicAppsStandard.validateNodeJsIsLatest', async (context: IActionContext) => { + context.errorHandling.suppressDisplay = true; + context.telemetry.properties.isActivationEvent = 'true'; + const showNodeJsWarningKey = 'showNodeJsWarning'; + const showNodeJsWarning = !!getWorkspaceSetting(showNodeJsWarningKey); + const binaries = binariesExist(nodeJsDependencyName); + context.telemetry.properties.binariesExist = `${binaries}`; + + if (!binaries) { + await installNodeJs(context, majorVersion); + context.telemetry.properties.binaryCommand = `${getNodeJsCommand()}`; + } else if (showNodeJsWarning) { + context.telemetry.properties.binaryCommand = `${getNodeJsCommand()}`; + const localVersion: string | null = await getLocalNodeJsVersion(); + context.telemetry.properties.localVersion = localVersion; + const newestVersion: string | undefined = await getLatestNodeJsVersion(context, majorVersion); + if (semver.major(newestVersion) === semver.major(localVersion) && semver.gt(newestVersion, localVersion)) { + context.telemetry.properties.outOfDateDotNet = 'true'; + const message: string = localize( + 'outdatedNodeJsRuntime', + 'Update your local Node JS version ({0}) to the latest version ({1}) for the best experience.', + localVersion, + newestVersion + ); + const update: MessageItem = { title: 'Update' }; + let result: MessageItem; + do { + result = + newestVersion !== undefined + ? await context.ui.showWarningMessage(message, update, DialogResponses.learnMore, DialogResponses.dontWarnAgain) + : await context.ui.showWarningMessage(message, DialogResponses.learnMore, DialogResponses.dontWarnAgain); + if (result === DialogResponses.learnMore) { + await openUrl('https://nodejs.org/en/download'); + } else if (result === update) { + await installNodeJs(context, majorVersion); + } else if (result === DialogResponses.dontWarnAgain) { + await updateGlobalSetting(showNodeJsWarningKey, false); + } + } while (result === DialogResponses.learnMore); + } + } + }); +} diff --git a/apps/vs-code-designer/src/app/commands/pickFuncProcess.ts b/apps/vs-code-designer/src/app/commands/pickFuncProcess.ts index 478227a4eff..144e2203049 100644 --- a/apps/vs-code-designer/src/app/commands/pickFuncProcess.ts +++ b/apps/vs-code-designer/src/app/commands/pickFuncProcess.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { defaultFuncPort, hostStartTaskName, pickProcessTimeoutSetting } from '../../constants'; +import { Platform, defaultFuncPort, hostStartTaskName, pickProcessTimeoutSetting } from '../../constants'; import { ext } from '../../extensionVariables'; import { localize } from '../../localize'; import { preDebugValidate } from '../debug/validatePreDebug'; @@ -25,7 +25,7 @@ import type { IPreDebugValidateResult, IProcessInfo } from '@microsoft/vscode-ex import * as unixPsTree from 'ps-tree'; import * as vscode from 'vscode'; //TODO: revisit this import again (yargsParser) -import yargsParser from 'yargs-parser'; +import * as parser from 'yargs-parser'; type OSAgnosticProcess = { command: string | undefined; pid: number | string }; type ActualUnixPS = unixPsTree.PS & { COMM?: string }; @@ -221,7 +221,7 @@ async function pickChildProcess(taskInfo: IRunningFuncTask): Promise { } } const children: OSAgnosticProcess[] = - process.platform === 'win32' ? await getWindowsChildren(taskInfo.processId) : await getUnixChildren(taskInfo.processId); + process.platform === Platform.windows ? await getWindowsChildren(taskInfo.processId) : await getUnixChildren(taskInfo.processId); const child: OSAgnosticProcess | undefined = children.reverse().find((c) => /(dotnet|func)(\.exe|)$/i.test(c.command || '')); return child ? child.pid.toString() : String(taskInfo.processId); } @@ -256,7 +256,7 @@ async function getWindowsChildren(pid: number): Promise { function getFunctionRuntimePort(funcTask: vscode.Task): number { const { command } = funcTask.definition; try { - const args = yargsParser(command); + const args = parser(command); const port = args['port'] || args['p'] || undefined; return port ?? Number(defaultFuncPort); } catch { diff --git a/apps/vs-code-designer/src/app/commands/registerCommands.ts b/apps/vs-code-designer/src/app/commands/registerCommands.ts index b6fe3c96388..a07aab9e14f 100644 --- a/apps/vs-code-designer/src/app/commands/registerCommands.ts +++ b/apps/vs-code-designer/src/app/commands/registerCommands.ts @@ -6,6 +6,7 @@ import { extensionCommand } from '../../constants'; import { ext } from '../../extensionVariables'; import { executeOnFunctions } from '../functionsExtension/executeOnFunctionsExt'; import { LogicAppResourceTree } from '../tree/LogicAppResourceTree'; +import { validateAndInstallBinaries } from '../utils/binaries'; import { downloadAppSettings } from './appSettings/downloadAppSettings'; import { editAppSetting } from './appSettings/editAppSetting'; import { renameAppSetting } from './appSettings/renameAppSetting'; @@ -127,6 +128,9 @@ export function registerCommands(): void { registerCommandWithTreeNodeUnwrapping(extensionCommand.configureDeploymentSource, configureDeploymentSource); registerCommandWithTreeNodeUnwrapping(extensionCommand.startRemoteDebug, startRemoteDebug); + registerCommandWithTreeNodeUnwrapping(extensionCommand.validateAndInstallBinaries, async (context: IActionContext) => + validateAndInstallBinaries(context) + ); // Data Mapper Commands registerCommand(extensionCommand.createNewDataMap, (context: IActionContext) => createNewDataMapCmd(context)); registerCommand(extensionCommand.loadDataMapFile, (context: IActionContext, uri: Uri) => loadDataMapFileCmd(context, uri)); diff --git a/apps/vs-code-designer/src/app/commands/workflows/getDebugSymbolDll.ts b/apps/vs-code-designer/src/app/commands/workflows/getDebugSymbolDll.ts index 98c062df253..1d00e5a4142 100644 --- a/apps/vs-code-designer/src/app/commands/workflows/getDebugSymbolDll.ts +++ b/apps/vs-code-designer/src/app/commands/workflows/getDebugSymbolDll.ts @@ -5,6 +5,7 @@ import { debugSymbolDll } from '../../../constants'; import { ext } from '../../../extensionVariables'; import { localize } from '../../../localize'; +import { getFunctionsCommand } from '../../utils/funcCoreTools/funcVersion'; import * as cp from 'child_process'; import * as fse from 'fs-extra'; import * as path from 'path'; @@ -34,7 +35,7 @@ export async function getDebugSymbolDll(): Promise { * @returns {string} Extension bundle folder path. */ async function getExtensionBundleFolder(): Promise { - const command = 'func GetExtensionBundlePath'; + const command = `${getFunctionsCommand()} GetExtensionBundlePath`; const outputChannel = ext.outputChannel; if (outputChannel) { diff --git a/apps/vs-code-designer/src/app/commands/workflows/switchToDotnetProject.ts b/apps/vs-code-designer/src/app/commands/workflows/switchToDotnetProject.ts index 7ca0ca0fde6..843ee8a082c 100644 --- a/apps/vs-code-designer/src/app/commands/workflows/switchToDotnetProject.ts +++ b/apps/vs-code-designer/src/app/commands/workflows/switchToDotnetProject.ts @@ -6,6 +6,7 @@ import { connectionsFileName, funcIgnoreFileName, funcVersionSetting, hostFileNa import { localize } from '../../../localize'; import { initProjectForVSCode } from '../../commands/initProjectForVSCode/initProjectForVSCode'; import { DotnetTemplateProvider } from '../../templates/dotnet/DotnetTemplateProvider'; +import { useBinariesDependencies } from '../../utils/binaries'; import { getDotnetBuildFile, addNugetPackagesToBuildFile, @@ -16,7 +17,7 @@ import { addFileToBuildPath, addLibToPublishPath, } from '../../utils/codeless/updateBuildFile'; -import { getProjFiles, getTemplateKeyFromProjFile } from '../../utils/dotnet/dotnet'; +import { getLocalDotNetVersionFromBinaries, getProjFiles, getTemplateKeyFromProjFile } from '../../utils/dotnet/dotnet'; import { validateDotnetInstalled, getFramework, executeDotnetTemplateCommand } from '../../utils/dotnet/executeDotnetTemplateCommand'; import { wrapArgInQuotes } from '../../utils/funcCoreTools/cpUtils'; import { tryGetMajorVersion, tryParseFuncVersion } from '../../utils/funcCoreTools/funcVersion'; @@ -104,6 +105,8 @@ export async function switchToDotnetProject(context: IProjectWizardContext, targ const projectPath: string = target.fsPath; const projTemplateKey = await getTemplateKeyFromProjFile(context, projectPath, version, ProjectLanguage.CSharp); const dotnetVersion = await getFramework(context, projectPath); + const useBinaries = useBinariesDependencies(); + const dotnetLocalVersion = useBinaries ? await getLocalDotNetVersionFromBinaries() : ''; await deleteBundleProjectFiles(target); await renameBundleProjectFiles(target); @@ -124,6 +127,9 @@ export async function switchToDotnetProject(context: IProjectWizardContext, targ await copyBundleProjectFiles(target); await updateBuildFile(context, target, dotnetVersion); + if (useBinaries) { + await createGlobalJsonFile(dotnetLocalVersion, target.fsPath); + } const workspaceFolder: vscode.WorkspaceFolder | undefined = getContainingWorkspace(target.fsPath); @@ -145,6 +151,18 @@ export async function switchToDotnetProject(context: IProjectWizardContext, targ ); } +async function createGlobalJsonFile(sdkVersion: string, projectRoot: string) { + const globalJsonPath = path.join(projectRoot, 'global.json'); + const globalJsonContent = { + sdk: { + version: sdkVersion, + }, + }; + + const contentString = JSON.stringify(globalJsonContent, null, 4); + fse.writeFileSync(globalJsonPath, contentString, 'utf8'); +} + async function updateBuildFile(context: IActionContext, target: vscode.Uri, dotnetVersion: string) { const projectArtifacts = await getArtifactNamesFromProject(target); let xmlBuildFile: any = await getDotnetBuildFile(context, target.fsPath); diff --git a/apps/vs-code-designer/src/app/debug/validatePreDebug.ts b/apps/vs-code-designer/src/app/debug/validatePreDebug.ts index 8b31fd053bc..d9ce0ff2cc5 100644 --- a/apps/vs-code-designer/src/app/debug/validatePreDebug.ts +++ b/apps/vs-code-designer/src/app/debug/validatePreDebug.ts @@ -8,6 +8,7 @@ import { localEmulatorConnectionString, azureWebJobsStorageKey, localSettingsFileName, + Platform, } from '../../constants'; import { localize } from '../../localize'; import { validateFuncCoreToolsInstalled } from '../commands/funcCoreTools/validateFuncCoreToolsInstalled'; @@ -134,7 +135,7 @@ async function validateEmulatorIsRunning(context: IActionContext, projectPath: s localSettingsFileName ); - const learnMoreLink: string = process.platform === 'win32' ? 'https://aka.ms/AA4ym56' : 'https://aka.ms/AA4yef8'; + const learnMoreLink: string = process.platform === Platform.windows ? 'https://aka.ms/AA4ym56' : 'https://aka.ms/AA4yef8'; const debugAnyway: vscode.MessageItem = { title: localize('debugAnyway', 'Debug anyway') }; const result: vscode.MessageItem = await context.ui.showWarningMessage(message, { learnMoreLink, modal: true }, debugAnyway); return result === debugAnyway; diff --git a/apps/vs-code-designer/src/app/utils/azurite/activateAzurite.ts b/apps/vs-code-designer/src/app/utils/azurite/activateAzurite.ts new file mode 100644 index 00000000000..f668e2f0d89 --- /dev/null +++ b/apps/vs-code-designer/src/app/utils/azurite/activateAzurite.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { + autoStartAzuriteSetting, + azuriteBinariesLocationSetting, + azuriteExtensionPrefix, + azuriteLocationSetting, + defaultAzuritePathValue, + extensionCommand, + showAutoStartAzuriteWarning, +} from '../../../constants'; +import { localize } from '../../../localize'; +import { executeOnAzurite } from '../../azuriteExtension/executeOnAzuriteExt'; +import { tryGetFunctionProjectRoot } from '../verifyIsProject'; +import { getWorkspaceSetting, updateGlobalSetting, updateWorkspaceSetting } from '../vsCodeConfig/settings'; +import { getWorkspaceFolder } from '../workspace'; +import { DialogResponses, type IActionContext } from '@microsoft/vscode-azext-utils'; +import * as vscode from 'vscode'; +import type { MessageItem } from 'vscode'; + +/** + * Prompts user to set azurite.location and Start Azurite. + * If azurite extension location was not set: + * Overrides default Azurite location to new default location. + * User can specify location. + */ +export async function activateAzurite(context: IActionContext): Promise { + if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { + const workspaceFolder = await getWorkspaceFolder(context); + const projectPath = await tryGetFunctionProjectRoot(context, workspaceFolder); + + if (projectPath) { + const globalAzuriteLocationSetting: string = getWorkspaceSetting(azuriteLocationSetting, projectPath, azuriteExtensionPrefix); + context.telemetry.properties.globalAzuriteLocation = globalAzuriteLocationSetting; + + const azuriteLocationExtSetting: string = getWorkspaceSetting(azuriteBinariesLocationSetting); + + const showAutoStartAzuriteWarningSetting = !!getWorkspaceSetting(showAutoStartAzuriteWarning); + + const autoStartAzurite = !!getWorkspaceSetting(autoStartAzuriteSetting); + context.telemetry.properties.autoStartAzurite = `${autoStartAzurite}`; + + if (showAutoStartAzuriteWarningSetting) { + const enableMessage: MessageItem = { title: localize('enableAutoStart', 'Enable AutoStart') }; + + const result = await context.ui.showWarningMessage( + localize('autoStartAzuriteTitle', 'Configure Azurite to autostart on project launch?'), + enableMessage, + DialogResponses.no, + DialogResponses.dontWarnAgain + ); + + if (result == DialogResponses.dontWarnAgain) { + await updateGlobalSetting(showAutoStartAzuriteWarning, false); + } else if (result == enableMessage) { + await updateGlobalSetting(showAutoStartAzuriteWarning, false); + await updateGlobalSetting(autoStartAzuriteSetting, true); + + // User has not configured workspace azurite.location. + if (!azuriteLocationExtSetting) { + const azuriteDir = await context.ui.showInputBox({ + placeHolder: localize('configureAzuriteLocation', 'Azurite Location'), + prompt: localize('configureWebhookEndpointPrompt', 'Configure Azurite Workspace location folder path'), + value: defaultAzuritePathValue, + }); + + if (azuriteDir) { + await updateGlobalSetting(azuriteBinariesLocationSetting, azuriteDir); + } else { + await updateGlobalSetting(azuriteBinariesLocationSetting, defaultAzuritePathValue); + } + } + } + } else { + if (autoStartAzurite && !azuriteLocationExtSetting) { + await updateGlobalSetting(azuriteBinariesLocationSetting, defaultAzuritePathValue); + vscode.window.showInformationMessage( + localize('autoAzuriteLocation', `Azurite is setup to auto start at ${defaultAzuritePathValue}`) + ); + } + } + + if (getWorkspaceSetting(autoStartAzuriteSetting)) { + const azuriteWorkspaceSetting = getWorkspaceSetting(azuriteBinariesLocationSetting); + await updateWorkspaceSetting(azuriteLocationSetting, azuriteWorkspaceSetting, projectPath, azuriteExtensionPrefix); + await executeOnAzurite(context, extensionCommand.azureAzuriteStart); + context.telemetry.properties.azuriteStart = 'true'; + context.telemetry.properties.azuriteLocation = azuriteWorkspaceSetting; + } + } + } +} diff --git a/apps/vs-code-designer/src/app/utils/binaries.ts b/apps/vs-code-designer/src/app/utils/binaries.ts new file mode 100644 index 00000000000..ce88abc82dd --- /dev/null +++ b/apps/vs-code-designer/src/app/utils/binaries.ts @@ -0,0 +1,455 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { + DependencyDefaultPath, + DependencyVersion, + Platform, + autoRuntimeDependenciesValidationAndInstallationSetting, + defaultDependencyPathValue, + autoRuntimeDependenciesPathSettingKey, + dependencyTimeoutSettingKey, + dotNetBinaryPathSettingKey, + dotnetDependencyName, + funcCoreToolsBinaryPathSettingKey, + funcPackageName, + nodeJsBinaryPathSettingKey, + defaultLogicAppsFolder, +} from '../../constants'; +import { ext } from '../../extensionVariables'; +import { localize } from '../../localize'; +import { onboardBinaries } from '../../onboarding'; +import { validateDotNetIsLatest } from '../commands/dotnet/validateDotNetIsLatest'; +import { validateFuncCoreToolsIsLatest } from '../commands/funcCoreTools/validateFuncCoreToolsIsLatest'; +import { isNodeJsInstalled } from '../commands/nodeJs/validateNodeJsInstalled'; +import { validateNodeJsIsLatest } from '../commands/nodeJs/validateNodeJsIsLatest'; +import { getDependenciesVersion } from './bundleFeed'; +import { setDotNetCommand } from './dotnet/dotnet'; +import { executeCommand } from './funcCoreTools/cpUtils'; +import { setFunctionsCommand } from './funcCoreTools/funcVersion'; +import { getNpmCommand, setNodeJsCommand } from './nodeJs/nodeJsVersion'; +import { runWithDurationTelemetry } from './telemetry'; +import { timeout } from './timeout'; +import { getGlobalSetting, getWorkspaceSetting, updateGlobalSetting } from './vsCodeConfig/settings'; +import { DialogResponses, type IActionContext } from '@microsoft/vscode-azext-utils'; +import type { IBundleDependencyFeed, IGitHubReleaseInfo } from '@microsoft/vscode-extension'; +import axios from 'axios'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import * as semver from 'semver'; +import * as vscode from 'vscode'; + +import AdmZip = require('adm-zip'); +import request = require('request'); + +export async function validateAndInstallBinaries(context: IActionContext) { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, // Location of the progress indicator + title: 'Validating Runtime Dependency', // Title displayed in the progress notification + cancellable: false, // Allow the user to cancel the task + }, + async (progress, token) => { + token.onCancellationRequested(() => { + // Handle cancellation logic + executeCommand(ext.outputChannel, undefined, 'echo', 'validateAndInstallBinaries was canceled'); + }); + + context.telemetry.properties.lastStep = 'getGlobalSetting'; + progress.report({ increment: 10, message: `Get Settings` }); + + const dependencyTimeout = (await getDependencyTimeout()) * 1000; + + context.telemetry.properties.dependencyTimeout = `${dependencyTimeout} milliseconds`; + if (!getGlobalSetting(autoRuntimeDependenciesPathSettingKey)) { + await updateGlobalSetting(autoRuntimeDependenciesPathSettingKey, defaultDependencyPathValue); + context.telemetry.properties.dependencyPath = defaultDependencyPathValue; + } + + context.telemetry.properties.lastStep = 'getDependenciesVersion'; + progress.report({ increment: 10, message: `Get dependency version from CDN` }); + let dependenciesVersions: IBundleDependencyFeed; + try { + dependenciesVersions = await getDependenciesVersion(context); + context.telemetry.properties.dependenciesVersions = JSON.stringify(dependenciesVersions); + } catch (error) { + // Unable to get dependency.json, will default to fallback versions + console.log(error); + } + + context.telemetry.properties.lastStep = 'validateNodeJsIsLatest'; + + try { + await runWithDurationTelemetry(context, 'azureLogicAppsStandard.validateNodeJsIsLatest', async () => { + progress.report({ increment: 20, message: `NodeJS` }); + await timeout(validateNodeJsIsLatest, dependencyTimeout, dependenciesVersions?.nodejs); + await setNodeJsCommand(); + }); + + context.telemetry.properties.lastStep = 'validateFuncCoreToolsIsLatest'; + await runWithDurationTelemetry(context, 'azureLogicAppsStandard.validateFuncCoreToolsIsLatest', async () => { + progress.report({ increment: 20, message: `Functions Runtime` }); + await timeout(validateFuncCoreToolsIsLatest, dependencyTimeout, dependenciesVersions?.funcCoreTools); + await setFunctionsCommand(); + }); + + context.telemetry.properties.lastStep = 'validateDotNetIsLatest'; + await runWithDurationTelemetry(context, 'azureLogicAppsStandard.validateDotNetIsLatest', async () => { + progress.report({ increment: 20, message: `.NET SDK` }); + await timeout(validateDotNetIsLatest, dependencyTimeout, dependenciesVersions?.dotnet); + await setDotNetCommand(); + }); + ext.outputChannel.appendLog( + localize( + 'azureLogicApsBinariesSucessfull', + 'Azure Logic Apps Standard Runtime Dependencies validation and installation completed successfully.' + ) + ); + } catch (error) { + ext.outputChannel.appendLog( + localize('azureLogicApsBinariesError', 'Error in dependencies validation and installation: "{0}"...', error) + ); + context.telemetry.properties.dependenciesError = error; + } + } + ); +} + +/** + * Download and Extracts Binaries zip. + * @param {string} binariesUrl - Binaries release url. + * @param {string} targetFolder - Module name to check. + * @param {string} dependencyName - The Dedepency name. + * @param {string} dotNetVersion - The .NET Major Version from CDN. + */ + +export async function downloadAndExtractBinaries( + binariesUrl: string, + targetFolder: string, + dependencyName: string, + dotNetVersion?: string +): Promise { + const tempFolderPath = path.join(os.tmpdir(), defaultLogicAppsFolder, dependencyName); + targetFolder = path.join(targetFolder, dependencyName); + fs.mkdirSync(targetFolder, { recursive: true }); + + // Read and write permissions + fs.chmodSync(targetFolder, 0o777); + + const binariesFileExtension = getCompressionFileExtension(binariesUrl); + const binariesFilePath = path.join(tempFolderPath, `${dependencyName}${binariesFileExtension}`); + + try { + await executeCommand(ext.outputChannel, undefined, 'echo', `Creating temporary folder... ${tempFolderPath}`); + fs.mkdirSync(tempFolderPath, { recursive: true }); + fs.chmodSync(tempFolderPath, 0o777); + + // Download the compressed binaries + await new Promise((resolve, reject) => { + executeCommand(ext.outputChannel, undefined, 'echo', `Downloading binaries from: ${binariesUrl}`); + const downloadStream = request(binariesUrl).pipe(fs.createWriteStream(binariesFilePath)); + downloadStream.on('finish', async () => { + await executeCommand(ext.outputChannel, undefined, 'echo', `Successfullly downloaded ${dependencyName} binaries.`); + + fs.chmodSync(binariesFilePath, 0o777); + + // Extract to targetFolder + if (dependencyName == dotnetDependencyName) { + const version = dotNetVersion ?? semver.major(DependencyVersion.dotnet6); + process.platform == Platform.windows + ? await executeCommand( + ext.outputChannel, + undefined, + 'powershell -ExecutionPolicy Bypass -File', + binariesFilePath, + '-InstallDir', + targetFolder, + '-Channel', + `${version}.0` + ) + : await executeCommand(ext.outputChannel, undefined, binariesFilePath, '-InstallDir', targetFolder, '-Channel', `${version}.0`); + } else { + await extractBinaries(binariesFilePath, targetFolder, dependencyName); + vscode.window.showInformationMessage(localize('successInstall', `Successfully installed ${dependencyName}`)); + } + resolve(); + }); + downloadStream.on('error', reject); + }); + } catch (error) { + vscode.window.showErrorMessage(`Error downloading and extracting the ${dependencyName} zip file: ${error.message}`); + await executeCommand(ext.outputChannel, undefined, 'echo', `[ExtractError]: Remove ${targetFolder}`); + fs.rmSync(targetFolder, { recursive: true }); + throw error; + } finally { + fs.rmSync(tempFolderPath, { recursive: true }); + await executeCommand(ext.outputChannel, undefined, 'echo', `Removed ${tempFolderPath}`); + } +} + +export async function getLatestFunctionCoreToolsVersion(context: IActionContext, majorVersion: string): Promise { + context.telemetry.properties.funcCoreTools = majorVersion; + + // Use npm to find newest func core tools version + const hasNodeJs = await isNodeJsInstalled(); + let latestVersion: string | null; + if (majorVersion && hasNodeJs) { + context.telemetry.properties.latestVersionSource = 'node'; + const npmCommand = getNpmCommand(); + try { + latestVersion = (await executeCommand(undefined, undefined, `${npmCommand}`, 'view', funcPackageName, 'version'))?.trim(); + if (checkMajorVersion(latestVersion, majorVersion)) { + return latestVersion; + } + } catch (error) { + console.log(error); + } + } else if (majorVersion) { + // fallback to github api to look for latest version + await readJsonFromUrl('https://api.github.com/repos/Azure/azure-functions-core-tools/releases/latest').then( + (response: IGitHubReleaseInfo) => { + latestVersion = semver.valid(semver.coerce(response.tag_name)); + context.telemetry.properties.latestVersionSource = 'github'; + context.telemetry.properties.latestGithubVersion = response.tag_name; + if (checkMajorVersion(latestVersion, majorVersion)) { + return latestVersion; + } + } + ); + } + + // Fall back to hardcoded version + context.telemetry.properties.getLatestFunctionCoreToolsVersion = 'fallback'; + return DependencyVersion.funcCoreTools; +} + +export async function getLatestDotNetVersion(context: IActionContext, majorVersion?: string): Promise { + context.telemetry.properties.dotNetMajorVersion = majorVersion; + + if (majorVersion) { + await readJsonFromUrl('https://api.github.com/repos/dotnet/sdk/releases') + .then((response: IGitHubReleaseInfo[]) => { + context.telemetry.properties.latestVersionSource = 'github'; + response.forEach((releaseInfo: IGitHubReleaseInfo) => { + const releaseVersion: string | null = semver.valid(semver.coerce(releaseInfo.tag_name)); + context.telemetry.properties.latestGithubVersion = releaseInfo.tag_name; + if (checkMajorVersion(releaseVersion, majorVersion)) { + return releaseVersion; + } + }); + }) + .catch((error) => { + throw Error(localize('errorNewestDotNetVersion', `Error getting latest .NET SDK version: ${error}`)); + }); + } + + context.telemetry.properties.latestVersionSource = 'fallback'; + return DependencyVersion.dotnet6; +} + +export async function getLatestNodeJsVersion(context: IActionContext, majorVersion?: string): Promise { + context.telemetry.properties.nodeMajorVersion = majorVersion; + + if (majorVersion) { + await readJsonFromUrl('https://api.github.com/repos/nodejs/node/releases') + .then((response: IGitHubReleaseInfo[]) => { + context.telemetry.properties.latestVersionSource = 'github'; + response.forEach((releaseInfo: IGitHubReleaseInfo) => { + const releaseVersion = semver.valid(semver.coerce(releaseInfo.tag_name)); + context.telemetry.properties.latestGithubVersion = releaseInfo.tag_name; + if (checkMajorVersion(releaseVersion, majorVersion)) { + return releaseVersion; + } + }); + }) + .catch((error) => { + throw Error(localize('errorNewestNodeJsVersion', `Error getting latest Node JS version: ${error}`)); + }); + } + + context.telemetry.properties.latestVersionSource = 'fallback'; + return DependencyVersion.nodeJs; +} + +export function getNodeJsBinariesReleaseUrl(version: string, osPlatform: string, arch: string): string { + if (osPlatform != 'win') { + return `https://nodejs.org/dist/v${version}/node-v${version}-${osPlatform}-${arch}.tar.gz`; + } + + return `https://nodejs.org/dist/v${version}/node-v${version}-${osPlatform}-${arch}.zip`; +} + +export function getFunctionCoreToolsBinariesReleaseUrl(version: string, osPlatform: string, arch: string): string { + return `https://github.com/Azure/azure-functions-core-tools/releases/download/${version}/Azure.Functions.Cli.${osPlatform}-${arch}.${version}.zip`; +} + +export function getDotNetBinariesReleaseUrl(): string { + return process.platform == Platform.windows ? 'https://dot.net/v1/dotnet-install.ps1' : 'https://dot.net/v1/dotnet-install.sh'; +} + +export function getCpuArchitecture() { + switch (process.arch) { + case 'x64': + case 'arm64': + return process.arch; + + default: + throw new Error(localize('UnsupportedCPUArchitecture', `Unsupported CPU architecture: ${process.arch}`)); + } +} + +/** + * Checks if binaries folder directory path exists. + * @param dependencyName The name of the dependency. + * @returns true if expected binaries folder directory path exists + */ +export function binariesExist(dependencyName: string): boolean { + if (!useBinariesDependencies()) { + return false; + } + const binariesLocation = getGlobalSetting(autoRuntimeDependenciesPathSettingKey); + const binariesPath = path.join(binariesLocation, dependencyName); + const binariesExist = fs.existsSync(binariesPath); + + executeCommand(ext.outputChannel, undefined, 'echo', `${dependencyName} Binaries: ${binariesPath}`); + return binariesExist; +} + +async function readJsonFromUrl(url: string): Promise { + try { + const response = await axios.get(url); + if (response.status === 200) { + return response.data; + } else { + throw new Error(`Request failed with status: ${response.status}`); + } + } catch (error) { + vscode.window.showErrorMessage(`Error reading JSON from URL: ${error.message}`); + throw error; + } +} + +function getCompressionFileExtension(binariesUrl: string): string { + if (binariesUrl.endsWith('.zip')) { + return '.zip'; + } + + if (binariesUrl.endsWith('.tar.gz')) { + return '.tar.gz'; + } + + if (binariesUrl.endsWith('.tar.xz')) { + return '.tar.xz'; + } + + if (binariesUrl.endsWith('.ps1')) { + return '.ps1'; + } + + if (binariesUrl.endsWith('.sh')) { + return '.sh'; + } + + throw new Error(localize('UnsupportedCompressionFileExtension', `Unsupported compression file extension: ${binariesUrl}`)); +} + +async function extractBinaries(binariesFilePath: string, targetFolder: string, dependencyName: string): Promise { + await executeCommand(ext.outputChannel, undefined, 'echo', `Extracting ${binariesFilePath}`); + try { + if (binariesFilePath.endsWith('.zip')) { + const zip = new AdmZip(binariesFilePath); + await zip.extractAllTo(targetFolder, /* overwrite */ true, /* Permissions */ true); + } else { + await executeCommand(ext.outputChannel, undefined, 'tar', `-xzvf`, binariesFilePath, '-C', targetFolder); + } + cleanupContainerFolder(targetFolder); + await executeCommand(ext.outputChannel, undefined, 'echo', `Extraction ${dependencyName} successfully completed.`); + } catch (error) { + throw new Error(`Error extracting ${dependencyName}: ${error}`); + } +} + +function checkMajorVersion(version: string, majorVersion: string): boolean { + return semver.major(version) === Number(majorVersion); +} + +/** + * Cleans up by removing Container Folder: + * path/to/folder/container/files --> /path/to/folder/files + * @param targetFolder + */ +function cleanupContainerFolder(targetFolder: string) { + const extractedContents = fs.readdirSync(targetFolder); + if (extractedContents.length === 1 && fs.statSync(path.join(targetFolder, extractedContents[0])).isDirectory()) { + const containerFolderPath = path.join(targetFolder, extractedContents[0]); + const containerContents = fs.readdirSync(containerFolderPath); + containerContents.forEach((content) => { + const contentPath = path.join(containerFolderPath, content); + const destinationPath = path.join(targetFolder, content); + fs.renameSync(contentPath, destinationPath); + }); + + if (fs.readdirSync(containerFolderPath).length === 0) { + fs.rmSync(containerFolderPath, { recursive: true }); + } + } +} + +/** + * Gets dependency timeout setting value from workspace settings. + * @param {IActionContext} context - Command context. + * @returns {number} Timeout value in seconds. + */ +function getDependencyTimeout(): number { + const dependencyTimeoutValue: number | undefined = getWorkspaceSetting(dependencyTimeoutSettingKey); + const timeoutInSeconds = Number(dependencyTimeoutValue); + if (isNaN(timeoutInSeconds)) { + throw new Error( + localize( + 'invalidSettingValue', + 'The setting "{0}" must be a number, but instead found "{1}".', + dependencyTimeoutValue, + dependencyTimeoutValue + ) + ); + } + + return timeoutInSeconds; +} + +/** + * Propmts warning message to decide the auto validation/installation of dependency binaries. + * @param {IActionContext} context - Activation context. + */ +export async function promptInstallBinariesOption(context: IActionContext) { + const message = localize('useBinaries', 'Allow auto runtime dependencies validation and installation at extension launch (Preview)'); + const confirm = { title: localize('yesRecommended', 'Yes (Recommended)') }; + let result: vscode.MessageItem; + + const binariesInstallation = getGlobalSetting(autoRuntimeDependenciesValidationAndInstallationSetting); + + if (binariesInstallation === null) { + result = await context.ui.showWarningMessage(message, confirm, DialogResponses.dontWarnAgain); + if (result === confirm) { + await updateGlobalSetting(autoRuntimeDependenciesValidationAndInstallationSetting, true); + await onboardBinaries(context); + context.telemetry.properties.autoRuntimeDependenciesValidationAndInstallationSetting = 'true'; + } else if (result === DialogResponses.dontWarnAgain) { + await updateGlobalSetting(autoRuntimeDependenciesValidationAndInstallationSetting, false); + await updateGlobalSetting(dotNetBinaryPathSettingKey, DependencyDefaultPath.dotnet); + await updateGlobalSetting(nodeJsBinaryPathSettingKey, DependencyDefaultPath.node); + await updateGlobalSetting(funcCoreToolsBinaryPathSettingKey, DependencyDefaultPath.funcCoreTools); + context.telemetry.properties.autoRuntimeDependenciesValidationAndInstallationSetting = 'false'; + } + } +} + +/** + * Returns boolean to determine if workspace uses binaries dependencies. + */ +export const useBinariesDependencies = (): boolean => { + const binariesInstallation = getGlobalSetting(autoRuntimeDependenciesValidationAndInstallationSetting); + return !!binariesInstallation; +}; diff --git a/apps/vs-code-designer/src/app/utils/bundleFeed.ts b/apps/vs-code-designer/src/app/utils/bundleFeed.ts index 881ab29f86c..abf537afc22 100644 --- a/apps/vs-code-designer/src/app/utils/bundleFeed.ts +++ b/apps/vs-code-designer/src/app/utils/bundleFeed.ts @@ -2,10 +2,13 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { defaultVersionRange, defaultBundleId } from '../../constants'; +import { defaultVersionRange, defaultBundleId, localSettingsFileName } from '../../constants'; +import { getLocalSettingsJson } from './appSettings/localSettings'; import { getJsonFeed } from './feed'; import type { IActionContext } from '@microsoft/vscode-azext-utils'; -import type { IBundleFeed, IBundleMetadata, IHostJsonV2 } from '@microsoft/vscode-extension'; +import type { IBundleDependencyFeed, IBundleFeed, IBundleMetadata, IHostJsonV2 } from '@microsoft/vscode-extension'; +import { join } from 'path'; +import * as vscode from 'vscode'; /** * Gets bundle extension feed. @@ -29,6 +32,29 @@ async function getBundleFeed(context: IActionContext, bundleMetadata: IBundleMet return getJsonFeed(context, url); } +/** + * Gets extension bundle dependency feed. + * @param {IActionContext} context - Command context. + * @param {IBundleMetadata | undefined} bundleMetadata - Bundle meta data. + * @returns {Promise} Returns bundle extension object. + */ +async function getBundleDependencyFeed( + context: IActionContext, + bundleMetadata: IBundleMetadata | undefined +): Promise { + const bundleId: string = (bundleMetadata && bundleMetadata?.id) || defaultBundleId; + const projectPath: string | undefined = vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri.fsPath : null; + let envVarUri: string | undefined = process.env.FUNCTIONS_EXTENSIONBUNDLE_SOURCE_URI; + if (projectPath) { + envVarUri = (await getLocalSettingsJson(context, join(projectPath, localSettingsFileName)))?.Values + ?.FUNCTIONS_EXTENSIONBUNDLE_SOURCE_URI; + } + + const baseUrl: string = envVarUri || 'https://functionscdn.azureedge.net/public'; + const url = `${baseUrl}/ExtensionBundles/${bundleId}.Workflows/dependency.json`; + return getJsonFeed(context, url); +} + /** * Gets latest bundle extension version range. * @param {IActionContext} context - Command context. @@ -39,6 +65,16 @@ export async function getLatestVersionRange(context: IActionContext): Promise} Returns dependency versions. + */ +export async function getDependenciesVersion(context: IActionContext): Promise { + const feed: IBundleDependencyFeed = await getBundleDependencyFeed(context, undefined); + return feed; +} + /** * Add bundle extension version to host.json configuration. * @param {IActionContext} context - Command context. diff --git a/apps/vs-code-designer/src/app/utils/codeless/startDesignTimeApi.ts b/apps/vs-code-designer/src/app/utils/codeless/startDesignTimeApi.ts index 9ffb93b0238..904df7794b5 100644 --- a/apps/vs-code-designer/src/app/utils/codeless/startDesignTimeApi.ts +++ b/apps/vs-code-designer/src/app/utils/codeless/startDesignTimeApi.ts @@ -3,21 +3,34 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { + Platform, ProjectDirectoryPath, + autoStartDesignTimeSetting, defaultVersionRange, designerStartApi, extensionBundleId, hostFileName, localSettingsFileName, logicAppKind, + showStartDesignTimeMessageSetting, workflowDesignerLoadTimeout, } from '../../../constants'; import { ext } from '../../../extensionVariables'; import { localize } from '../../../localize'; import { updateFuncIgnore } from '../codeless/common'; import { writeFormattedJson } from '../fs'; +import { getFunctionsCommand } from '../funcCoreTools/funcVersion'; +import { tryGetFunctionProjectRoot } from '../verifyIsProject'; +import { getWorkspaceSetting, updateGlobalSetting } from '../vsCodeConfig/settings'; +import { getWorkspaceFolder } from '../workspace'; import { delay } from '@azure/ms-rest-js'; -import type { IAzExtOutputChannel } from '@microsoft/vscode-azext-utils'; +import { + DialogResponses, + openUrl, + type IActionContext, + type IAzExtOutputChannel, + callWithTelemetryAndErrorHandling, +} from '@microsoft/vscode-azext-utils'; import { WorkerRuntime } from '@microsoft/vscode-extension'; import * as cp from 'child_process'; import * as fs from 'fs'; @@ -25,71 +38,78 @@ import * as os from 'os'; import * as path from 'path'; import * as portfinder from 'portfinder'; import * as requestP from 'request-promise'; +import * as vscode from 'vscode'; import { Uri, window, workspace } from 'vscode'; import type { MessageItem } from 'vscode'; export async function startDesignTimeApi(projectPath: string): Promise { - const designTimeDirectoryName = 'workflow-designtime'; - const hostFileContent: any = { - version: '2.0', - extensionBundle: { - id: extensionBundleId, - version: defaultVersionRange, - }, - extensions: { - workflow: { - settings: { - 'Runtime.WorkflowOperationDiscoveryHostMode': 'true', + await callWithTelemetryAndErrorHandling('azureLogicAppsStandard.startDesignTimeApi', async (actionContext: IActionContext) => { + actionContext.telemetry.properties.startDesignTimeApi = 'false'; + + const designTimeDirectoryName = 'workflow-designtime'; + const hostFileContent: any = { + version: '2.0', + extensionBundle: { + id: extensionBundleId, + version: defaultVersionRange, + }, + extensions: { + workflow: { + settings: { + 'Runtime.WorkflowOperationDiscoveryHostMode': 'true', + }, }, }, - }, - }; - const settingsFileContent: any = { - IsEncrypted: false, - Values: { - AzureWebJobsSecretStorageType: 'Files', - FUNCTIONS_WORKER_RUNTIME: WorkerRuntime.Node, - APP_KIND: logicAppKind, - }, - }; - if (!ext.workflowDesignTimePort) { - ext.workflowDesignTimePort = await portfinder.getPortPromise(); - } - - const url = `http://localhost:${ext.workflowDesignTimePort}${designerStartApi}`; - if (await isDesignTimeUp(url)) { - return; - } + }; + const settingsFileContent: any = { + IsEncrypted: false, + Values: { + AzureWebJobsSecretStorageType: 'Files', + FUNCTIONS_WORKER_RUNTIME: WorkerRuntime.Node, + APP_KIND: logicAppKind, + }, + }; + if (!ext.workflowDesignTimePort) { + ext.workflowDesignTimePort = await portfinder.getPortPromise(); + } - try { - window.showInformationMessage( - localize('azureFunctions.designTimeApi', 'Starting workflow design time api. It might take a few seconds.'), - 'OK' - ); - - const designTimeDirectory: Uri | undefined = await getOrCreateDesignTimeDirectory(designTimeDirectoryName, projectPath); - settingsFileContent.Values[ProjectDirectoryPath] = path.join(designTimeDirectory.fsPath); - - if (designTimeDirectory) { - await createJsonFile(designTimeDirectory, hostFileName, hostFileContent); - await createJsonFile(designTimeDirectory, localSettingsFileName, settingsFileContent); - await updateFuncIgnore(projectPath, [`${designTimeDirectoryName}/`]); - const cwd: string = designTimeDirectory.fsPath; - const portArgs = `--port ${ext.workflowDesignTimePort}`; - startDesignTimeProcess(ext.outputChannel, cwd, 'func', 'host', 'start', portArgs); - await waitForDesignTimeStartUp(url, new Date().getTime()); - } else { - throw new Error(localize('DesignTimeDirectoryError', 'Design time directory could not be created.')); + const url = `http://localhost:${ext.workflowDesignTimePort}${designerStartApi}`; + if (await isDesignTimeUp(url)) { + actionContext.telemetry.properties.isDesignTimeUp = 'true'; + return; } - } catch (ex) { - const viewOutput: MessageItem = { title: localize('viewOutput', 'View output') }; - const message: string = localize('DesignTimeError', 'Workflow design time could not be started.'); - await window.showErrorMessage(message, viewOutput).then(async (result) => { - if (result === viewOutput) { - ext.outputChannel.show(); + + try { + window.showInformationMessage( + localize('azureFunctions.designTimeApi', 'Starting workflow design-time API, which might take a few seconds.'), + 'OK' + ); + + const designTimeDirectory: Uri | undefined = await getOrCreateDesignTimeDirectory(designTimeDirectoryName, projectPath); + settingsFileContent.Values[ProjectDirectoryPath] = path.join(designTimeDirectory.fsPath); + + if (designTimeDirectory) { + await createJsonFile(designTimeDirectory, hostFileName, hostFileContent); + await createJsonFile(designTimeDirectory, localSettingsFileName, settingsFileContent); + await updateFuncIgnore(projectPath, [`${designTimeDirectoryName}/`]); + const cwd: string = designTimeDirectory.fsPath; + const portArgs = `--port ${ext.workflowDesignTimePort}`; + startDesignTimeProcess(ext.outputChannel, cwd, getFunctionsCommand(), 'host', 'start', portArgs); + await waitForDesignTimeStartUp(url, new Date().getTime()); + actionContext.telemetry.properties.startDesignTimeApi = 'true'; + } else { + throw new Error(localize('DesignTimeDirectoryError', 'Failed to create design-time directory.')); } - }); - } + } catch (ex) { + const viewOutput: MessageItem = { title: localize('viewOutput', 'View output') }; + const message: string = localize('DesignTimeError', "Can't start the background design-time process."); + await window.showErrorMessage(message, viewOutput).then(async (result) => { + if (result === viewOutput) { + ext.outputChannel.show(); + } + }); + } + }); } async function getOrCreateDesignTimeDirectory(designTimeDirectory: string, projectRoot: string): Promise { @@ -171,10 +191,42 @@ export function stopDesignTimeApi(): void { return; } - if (os.platform() === 'win32') { + if (os.platform() === Platform.windows) { cp.exec('taskkill /pid ' + `${ext.workflowDesignChildProcess.pid}` + ' /T /F'); } else { ext.workflowDesignChildProcess.kill(); } ext.workflowDesignChildProcess = undefined; } + +export async function promptStartDesignTimeOption(context: IActionContext) { + if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { + const workspaceFolder = await getWorkspaceFolder(context); + const projectPath = await tryGetFunctionProjectRoot(context, workspaceFolder); + const autoStartDesignTime = !!getWorkspaceSetting(autoStartDesignTimeSetting); + const showStartDesignTimeMessage = !!getWorkspaceSetting(showStartDesignTimeMessageSetting); + if (projectPath) { + if (autoStartDesignTime) { + startDesignTimeApi(projectPath); + } else if (showStartDesignTimeMessage) { + const message = localize( + 'startDesignTimeApi', + 'Always start the background design-time process at launch? The workflow designer will open faster.' + ); + const confirm = { title: localize('yesRecommended', 'Yes (Recommended)') }; + let result: MessageItem; + do { + result = await context.ui.showWarningMessage(message, confirm, DialogResponses.learnMore, DialogResponses.dontWarnAgain); + if (result === confirm) { + await updateGlobalSetting(autoStartDesignTimeSetting, true); + startDesignTimeApi(projectPath); + } else if (result === DialogResponses.learnMore) { + await openUrl('https://learn.microsoft.com/en-us/azure/azure-functions/functions-develop-local'); + } else if (result === DialogResponses.dontWarnAgain) { + await updateGlobalSetting(showStartDesignTimeMessageSetting, false); + } + } while (result === DialogResponses.learnMore); + } + } + } +} diff --git a/apps/vs-code-designer/src/app/utils/delay.ts b/apps/vs-code-designer/src/app/utils/delay.ts deleted file mode 100644 index 01764b122c3..00000000000 --- a/apps/vs-code-designer/src/app/utils/delay.ts +++ /dev/null @@ -1,8 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export async function delay(ms: number): Promise { - await new Promise((resolve: () => void): NodeJS.Timer => setTimeout(resolve, ms)); -} diff --git a/apps/vs-code-designer/src/app/utils/dotnet/dotnet.ts b/apps/vs-code-designer/src/app/utils/dotnet/dotnet.ts index 631afdbe113..0125095db2b 100644 --- a/apps/vs-code-designer/src/app/utils/dotnet/dotnet.ts +++ b/apps/vs-code-designer/src/app/utils/dotnet/dotnet.ts @@ -2,15 +2,28 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DotnetVersion, isolatedSdkName } from '../../../constants'; +import { + DotnetVersion, + Platform, + autoRuntimeDependenciesPathSettingKey, + dotNetBinaryPathSettingKey, + dotnetDependencyName, + isolatedSdkName, +} from '../../../constants'; +import { ext } from '../../../extensionVariables'; import { localize } from '../../../localize'; +import { executeCommand } from '../funcCoreTools/cpUtils'; import { runWithDurationTelemetry } from '../telemetry'; +import { getGlobalSetting, updateGlobalSetting } from '../vsCodeConfig/settings'; import { findFiles } from '../workspace'; import type { IActionContext } from '@microsoft/vscode-azext-utils'; import { AzExtFsExtra } from '@microsoft/vscode-azext-utils'; import type { IWorkerRuntime } from '@microsoft/vscode-extension'; import { FuncVersion, ProjectLanguage } from '@microsoft/vscode-extension'; +import * as fs from 'fs'; import * as path from 'path'; +import * as semver from 'semver'; +import * as vscode from 'vscode'; export class ProjectFile { public name: string; @@ -170,3 +183,87 @@ export function getTemplateKeyFromFeedEntry(runtimeInfo: IWorkerRuntime): string const isIsolated = runtimeInfo.sdk.name.toLowerCase() === isolatedSdkName.toLowerCase(); return getProjectTemplateKey(runtimeInfo.targetFramework, isIsolated); } + +export async function getLocalDotNetVersionFromBinaries(): Promise { + const binariesLocation = getGlobalSetting(autoRuntimeDependenciesPathSettingKey); + const dotNetBinariesPath = path.join(binariesLocation, dotnetDependencyName); + const sdkVersionFolder = path.join(dotNetBinariesPath, 'sdk'); + + // First try to get sdk from Binary installation folder + const files = fs.existsSync(sdkVersionFolder) ? fs.readdirSync(sdkVersionFolder, { withFileTypes: true }) : null; + for (const file of files) { + if (file.isDirectory()) { + const version = file.name; + await executeCommand(ext.outputChannel, undefined, 'echo', 'Local binary .NET SDK version', version); + return version; + } + } + + try { + const output: string = await executeCommand(ext.outputChannel, undefined, `${getDotNetCommand()}`, '--version'); + const version: string | null = semver.clean(output); + if (version) { + return version; + } + } catch (error) { + return null; + } + + return null; +} + +/** + * Get the nodejs binaries executable or use the system nodejs executable. + */ +export function getDotNetCommand(): string { + const command = getGlobalSetting(dotNetBinaryPathSettingKey); + return command; +} + +export async function setDotNetCommand(): Promise { + const binariesLocation = getGlobalSetting(autoRuntimeDependenciesPathSettingKey); + const dotNetBinariesPath = path.join(binariesLocation, dotnetDependencyName); + const binariesExist = fs.existsSync(dotNetBinariesPath); + let command = ext.dotNetCliPath; + if (binariesExist) { + // Explicit executable for tasks.json + command = + process.platform == Platform.windows + ? path.join(dotNetBinariesPath, `${ext.dotNetCliPath}.exe`) + : path.join(dotNetBinariesPath, `${ext.dotNetCliPath}`); + const newPath = `${dotNetBinariesPath}${path.delimiter}\${env:PATH}`; + fs.chmodSync(dotNetBinariesPath, 0o777); + + try { + const terminalConfig = vscode.workspace.getConfiguration(); + const pathEnv = { + PATH: newPath, + }; + + // Required for dotnet cli in VSCode Terminal + switch (process.platform) { + case Platform.windows: { + terminalConfig.update('terminal.integrated.env.windows', pathEnv, vscode.ConfigurationTarget.Workspace); + break; + } + + case Platform.linux: { + terminalConfig.update('terminal.integrated.env.linux', pathEnv, vscode.ConfigurationTarget.Workspace); + break; + } + + case Platform.mac: { + terminalConfig.update('terminal.integrated.env.osx', pathEnv, vscode.ConfigurationTarget.Workspace); + break; + } + } + + // Required for CoreClr + terminalConfig.update('omnisharp.dotNetCliPaths', [dotNetBinariesPath], vscode.ConfigurationTarget.Workspace); + } catch (error) { + console.log(error); + } + } + + await updateGlobalSetting(dotNetBinaryPathSettingKey, command); +} diff --git a/apps/vs-code-designer/src/app/utils/dotnet/executeDotnetTemplateCommand.ts b/apps/vs-code-designer/src/app/utils/dotnet/executeDotnetTemplateCommand.ts index a7927fac7b7..d00fa751d9c 100644 --- a/apps/vs-code-designer/src/app/utils/dotnet/executeDotnetTemplateCommand.ts +++ b/apps/vs-code-designer/src/app/utils/dotnet/executeDotnetTemplateCommand.ts @@ -4,7 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { ext } from '../../../extensionVariables'; import { localize } from '../../../localize'; +import { useBinariesDependencies } from '../binaries'; import { executeCommand, wrapArgInQuotes } from '../funcCoreTools/cpUtils'; +import { getDotNetCommand, getLocalDotNetVersionFromBinaries } from './dotnet'; import type { IActionContext } from '@microsoft/vscode-azext-utils'; import type { FuncVersion } from '@microsoft/vscode-extension'; import * as path from 'path'; @@ -39,7 +41,7 @@ export async function executeDotnetTemplateCommand( return await executeCommand( undefined, workingDirectory, - 'dotnet', + getDotNetCommand(), wrapArgInQuotes(jsonDllPath), '--templateDir', wrapArgInQuotes(getDotnetTemplateDir(version, projTemplateKey)), @@ -85,14 +87,18 @@ export async function validateDotnetInstalled(context: IActionContext): Promise< export async function getFramework(context: IActionContext, workingDirectory: string | undefined): Promise { if (!cachedFramework) { let versions = ''; + const dotnetBinariesLocation = getDotNetCommand(); + + versions = useBinariesDependencies() ? await getLocalDotNetVersionFromBinaries() : versions; + try { - versions += await executeCommand(undefined, workingDirectory, 'dotnet', '--version'); + versions += await executeCommand(undefined, workingDirectory, dotnetBinariesLocation, '--version'); } catch { // ignore } try { - versions += await executeCommand(undefined, workingDirectory, 'dotnet', '--list-sdks'); + versions += await executeCommand(undefined, workingDirectory, dotnetBinariesLocation, '--list-sdks'); } catch { // ignore } diff --git a/apps/vs-code-designer/src/app/utils/fs.ts b/apps/vs-code-designer/src/app/utils/fs.ts index 4352d957f45..09e2a8f51ec 100644 --- a/apps/vs-code-designer/src/app/utils/fs.ts +++ b/apps/vs-code-designer/src/app/utils/fs.ts @@ -49,12 +49,13 @@ export function isSubpath(expectedParent: string, expectedChild: string, relativ * Displays warning message to select if desire to overwrite file. * @param {IActionContext} context - Command context. * @param {string} fsPath - File path. + * @param {string} message - Message. * @returns {Promise} True if user wants to overwrite file. */ -export async function confirmOverwriteFile(context: IActionContext, fsPath: string): Promise { +export async function confirmOverwriteFile(context: IActionContext, fsPath: string, message?: string): Promise { if (await fse.pathExists(fsPath)) { const result: MessageItem | undefined = await context.ui.showWarningMessage( - localize('fileAlreadyExists', 'File "{0}" already exists. Overwrite?', fsPath), + localize('fileAlreadyExists', message ?? 'File "{0}" already exists. Overwrite?', fsPath), { modal: true }, DialogResponses.yes, DialogResponses.no, diff --git a/apps/vs-code-designer/src/app/utils/funcCoreTools/cpUtils.ts b/apps/vs-code-designer/src/app/utils/funcCoreTools/cpUtils.ts index 5f398c11b72..b1f9d468b06 100644 --- a/apps/vs-code-designer/src/app/utils/funcCoreTools/cpUtils.ts +++ b/apps/vs-code-designer/src/app/utils/funcCoreTools/cpUtils.ts @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Platform } from '../../../constants'; import { localize } from '../../../localize'; import { isString } from '@microsoft/utils-logic-apps'; import type { IAzExtOutputChannel } from '@microsoft/vscode-azext-utils'; @@ -37,7 +38,7 @@ export async function executeCommand( ); } } else { - if (outputChannel) { + if (outputChannel && command != 'echo') { outputChannel.appendLog(localize('finishedRunningCommand', 'Finished running command: "{0} {1}".', command, result.formattedArgs)); } } @@ -62,7 +63,7 @@ export async function tryExecuteCommand( }; const childProc: cp.ChildProcess = cp.spawn(command, args, options); - if (outputChannel) { + if (outputChannel && command != 'echo') { outputChannel.appendLog(localize('runningCommand', 'Running command: "{0} {1}"...', command, formattedArgs)); } @@ -101,7 +102,7 @@ export async function tryExecuteCommand( * @returns {string} Argument wrapped in quotation marks. */ export function wrapArgInQuotes(arg?: string | boolean | number): string { - const quotationMark: string = process.platform === 'win32' ? '"' : "'"; + const quotationMark: string = process.platform === Platform.windows ? '"' : "'"; arg ??= ''; return isString(arg) ? quotationMark + arg + quotationMark : String(arg); } diff --git a/apps/vs-code-designer/src/app/utils/funcCoreTools/funcHostTask.ts b/apps/vs-code-designer/src/app/utils/funcCoreTools/funcHostTask.ts index a15fe8dec71..a6e40613f16 100644 --- a/apps/vs-code-designer/src/app/utils/funcCoreTools/funcHostTask.ts +++ b/apps/vs-code-designer/src/app/utils/funcCoreTools/funcHostTask.ts @@ -27,6 +27,12 @@ export const runningFuncTaskMap: Map * @returns {Promise} Functions core tools version. */ export async function getLocalFuncCoreToolsVersion(): Promise { - const output: string = await executeCommand(undefined, undefined, ext.funcCliPath, '--version'); - const version: string | null = semver.clean(output); - if (version) { - return version; - } else { - // Old versions of the func cli do not support '--version', so we have to parse the command usage to get the version - const matchResult: RegExpMatchArray | null = output.match(/(?:.*)Azure Functions Core Tools (.*)/); - if (matchResult !== null) { - let localVersion: string = matchResult[1].replace(/[()]/g, '').trim(); // remove () and whitespace - // this is a fix for a bug currently in the Function CLI - if (localVersion === '220.0.0-beta.0') { - localVersion = '2.0.1-beta.25'; + try { + const output: string = await executeCommand(undefined, undefined, `${getFunctionsCommand()}`, '--version'); + const version: string | null = semver.clean(output); + if (version) { + return version; + } else { + // Old versions of the func cli do not support '--version', so we have to parse the command usage to get the version + const matchResult: RegExpMatchArray | null = output.match(/(?:.*)Azure Functions Core Tools (.*)/); + if (matchResult !== null) { + let localVersion: string = matchResult[1].replace(/[()]/g, '').trim(); // remove () and whitespace + // this is a fix for a bug currently in the Function CLI + if (localVersion === '220.0.0-beta.0') { + localVersion = '2.0.1-beta.25'; + } + return semver.valid(localVersion); } - return semver.valid(localVersion); - } + return null; + } + } catch (error) { return null; } } @@ -134,3 +145,27 @@ export function checkSupportedFuncVersion(version: FuncVersion) { ); } } + +/** + * Get the functions binaries executable or use the system functions executable. + */ +export function getFunctionsCommand(): string { + const command = getGlobalSetting(funcCoreToolsBinaryPathSettingKey); + if (!command) { + throw Error('Functions Core Tools Binary Path Setting is empty'); + } + return command; +} + +export async function setFunctionsCommand(): Promise { + const binariesLocation = getGlobalSetting(autoRuntimeDependenciesPathSettingKey); + const funcBinariesPath = path.join(binariesLocation, funcDependencyName); + const binariesExist = fs.existsSync(funcBinariesPath); + let command = ext.funcCliPath; + if (binariesExist) { + command = path.join(funcBinariesPath, ext.funcCliPath); + fs.chmodSync(funcBinariesPath, 0o777); + } + + await updateGlobalSetting(funcCoreToolsBinaryPathSettingKey, command); +} diff --git a/apps/vs-code-designer/src/app/utils/funcCoreTools/getFuncPackageManagers.ts b/apps/vs-code-designer/src/app/utils/funcCoreTools/getFuncPackageManagers.ts index dc4d7b07dd7..e37b3b54eb0 100644 --- a/apps/vs-code-designer/src/app/utils/funcCoreTools/getFuncPackageManagers.ts +++ b/apps/vs-code-designer/src/app/utils/funcCoreTools/getFuncPackageManagers.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { funcPackageName, PackageManager } from '../../../constants'; +import { funcPackageName, PackageManager, Platform } from '../../../constants'; import { executeCommand } from './cpUtils'; import { tryGetInstalledBrewPackageName } from './getBrewPackageName'; import { FuncVersion } from '@microsoft/vscode-extension'; @@ -15,10 +15,10 @@ import { FuncVersion } from '@microsoft/vscode-extension'; export async function getFuncPackageManagers(isFuncInstalled: boolean): Promise { const result: PackageManager[] = []; switch (process.platform) { - case 'linux': + case Platform.linux: // https://github.com/Microsoft/vscode-azurefunctions/issues/311 break; - case 'darwin': + case Platform.mac: if (await hasBrew(isFuncInstalled)) { result.push(PackageManager.brew); } diff --git a/apps/vs-code-designer/src/app/utils/nodeJs/nodeJsVersion.ts b/apps/vs-code-designer/src/app/utils/nodeJs/nodeJsVersion.ts new file mode 100644 index 00000000000..464a57e79e2 --- /dev/null +++ b/apps/vs-code-designer/src/app/utils/nodeJs/nodeJsVersion.ts @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Platform, autoRuntimeDependenciesPathSettingKey, nodeJsBinaryPathSettingKey, nodeJsDependencyName } from '../../../constants'; +import { ext } from '../../../extensionVariables'; +import { executeCommand } from '../funcCoreTools/cpUtils'; +import { getGlobalSetting, updateGlobalSetting } from '../vsCodeConfig/settings'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as semver from 'semver'; + +/** + * Executes nodejs version command and gets it from cli. + * @returns {Promise} Functions core tools version. + */ +export async function getLocalNodeJsVersion(): Promise { + try { + const output: string = await executeCommand(undefined, undefined, `${getNodeJsCommand()}`, '--version'); + const version: string | null = semver.clean(output); + if (version) { + return version; + } else { + return null; + } + } catch (error) { + return null; + } +} + +/** + * Get the npm binaries executable or use the system npm executable. + */ +export function getNpmCommand(): string { + const binariesLocation = getGlobalSetting(autoRuntimeDependenciesPathSettingKey); + const nodeJsBinariesPath = path.join(binariesLocation, nodeJsDependencyName); + const binariesExist = fs.existsSync(nodeJsBinariesPath); + let command = ext.npmCliPath; + if (binariesExist) { + // windows the executable is at root folder, linux & macos its in the bin + command = path.join(nodeJsBinariesPath, ext.npmCliPath); + if (process.platform != Platform.windows) { + const nodeSubFolder = getNodeSubFolder(command); + command = path.join(nodeJsBinariesPath, nodeSubFolder, 'bin', ext.npmCliPath); + } + } + return command; +} + +/** + * Get the nodejs binaries executable or use the system nodejs executable. + */ +export function getNodeJsCommand(): string { + const command = getGlobalSetting(nodeJsBinaryPathSettingKey); + return command; +} + +export async function setNodeJsCommand(): Promise { + const binariesLocation = getGlobalSetting(autoRuntimeDependenciesPathSettingKey); + const nodeJsBinariesPath = path.join(binariesLocation, nodeJsDependencyName); + const binariesExist = fs.existsSync(nodeJsBinariesPath); + let command = ext.nodeJsCliPath; + if (binariesExist) { + // windows the executable is at root folder, linux & macos its in the bin + command = path.join(nodeJsBinariesPath, ext.nodeJsCliPath); + if (process.platform != Platform.windows) { + const nodeSubFolder = getNodeSubFolder(command); + command = path.join(nodeJsBinariesPath, nodeSubFolder, 'bin', ext.nodeJsCliPath); + + fs.chmodSync(nodeJsBinariesPath, 0o777); + } + } + await updateGlobalSetting(nodeJsBinaryPathSettingKey, command); +} + +function getNodeSubFolder(directoryPath: string): string | null { + try { + const items = fs.readdirSync(directoryPath); + + for (const item of items) { + const itemPath = path.join(directoryPath, item); + const stats = fs.statSync(itemPath); + + if (stats.isDirectory() && item.includes('node')) { + return item; + } + } + } catch (error) { + console.error('Error:', error.message); + } + + return ''; // No 'node' subfolders found +} diff --git a/apps/vs-code-designer/src/app/utils/requestUtils.ts b/apps/vs-code-designer/src/app/utils/requestUtils.ts index ab1b83a80e0..bd087a9338f 100644 --- a/apps/vs-code-designer/src/app/utils/requestUtils.ts +++ b/apps/vs-code-designer/src/app/utils/requestUtils.ts @@ -84,7 +84,7 @@ export async function sendRequestWithExtTimeout( localize('timeoutFeed', 'Request timed out. Modify setting "{0}.{1}" if you want to extend the timeout.', ext.prefix, timeoutKey) ); } else { - throw error; + throw new Error(localize('sendRequestError', `${options.url} request failed with error: ${error}`)); } } } diff --git a/apps/vs-code-designer/src/app/utils/taskUtils.ts b/apps/vs-code-designer/src/app/utils/taskUtils.ts index 53e3f0805b7..8803c22ffc2 100644 --- a/apps/vs-code-designer/src/app/utils/taskUtils.ts +++ b/apps/vs-code-designer/src/app/utils/taskUtils.ts @@ -2,9 +2,10 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as packageJson from '../../package.json'; import { isPathEqual } from './fs'; import type { Task, WorkspaceFolder } from 'vscode'; -import { tasks as codeTasks } from 'vscode'; +import { tasks as codeTasks, window } from 'vscode'; /** * Gets task's file system path. @@ -61,3 +62,17 @@ export async function executeIfNotActive(task: Task): Promise { await codeTasks.executeTask(task); } } + +/** + * Displays a preview warning for any command that is marked as a preview feature in package.json. + * @param commandIdentifier - The identifier of the command to check for preview status. + */ +export function showPreviewWarning(commandIdentifier: string): void { + // Search for the command in the package.json "contributes.commands" array + const targetCommand = packageJson.contributes.commands.find((command) => command.command === commandIdentifier); + // If the command is found and it is marked as a preview, show a warning using its title + if (targetCommand?.preview) { + const commandTitle = targetCommand.title; + window.showInformationMessage(`The "${commandTitle}" command is a preview feature and might be subject to change.`); + } +} diff --git a/apps/vs-code-designer/src/app/utils/timeout.ts b/apps/vs-code-designer/src/app/utils/timeout.ts new file mode 100644 index 00000000000..7db60b8962a --- /dev/null +++ b/apps/vs-code-designer/src/app/utils/timeout.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +export async function timeout(asyncFunc: (...params: any[]) => Promise, timeoutMs: number, ...params: any[]): Promise { + try { + const asyncOperation = asyncFunc(...params); + await Promise.race([asyncOperation, timeOutErrorOperation(timeoutMs)]); + + return await asyncOperation; + } catch (error) { + throw new Error(`${asyncFunc.name} timed out after ${timeoutMs} ms.`); + } +} + +/** + * Sets a timeout and throws an error if timeout. + */ +async function timeOutErrorOperation(ms: number): Promise { + return await new Promise((_, reject) => { + setTimeout(() => { + reject(new Error()); + }, ms); + }); +} diff --git a/apps/vs-code-designer/src/app/utils/verifyIsProject.ts b/apps/vs-code-designer/src/app/utils/verifyIsProject.ts index c5dfd198694..32380ba8503 100644 --- a/apps/vs-code-designer/src/app/utils/verifyIsProject.ts +++ b/apps/vs-code-designer/src/app/utils/verifyIsProject.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { hostFileName } from '../../constants'; +import { hostFileName, localSettingsFileName } from '../../constants'; import { localize } from '../../localize'; import { createNewProjectInternal } from '../commands/createNewProject/createNewProject'; import { getWorkspaceSetting, updateWorkspaceSetting } from './vsCodeConfig/settings'; @@ -16,14 +16,16 @@ import type { MessageItem, WorkspaceFolder } from 'vscode'; const projectSubpathKey = 'projectSubpath'; -// Use 'host.json' as an indicator that this is a functions project +// Use 'host.json' and 'local.settings.json' as an indicator that this is a functions project export async function isFunctionProject(folderPath: string): Promise { - return await fse.pathExists(path.join(folderPath, hostFileName)); + return ( + (await fse.pathExists(path.join(folderPath, hostFileName))) && (await fse.pathExists(path.join(folderPath, localSettingsFileName))) + ); } /** * Checks root folder and subFolders one level down - * If a single function project is found, returns that path. + * If a single logic app project is found, return that path. * If multiple projects are found, prompt to pick the project. */ export async function tryGetFunctionProjectRoot( diff --git a/apps/vs-code-designer/src/app/utils/vsCodeConfig/settings.ts b/apps/vs-code-designer/src/app/utils/vsCodeConfig/settings.ts index 0c55a2d380a..1df6bb63012 100644 --- a/apps/vs-code-designer/src/app/utils/vsCodeConfig/settings.ts +++ b/apps/vs-code-designer/src/app/utils/vsCodeConfig/settings.ts @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Platform } from '../../../constants'; import { ext } from '../../../extensionVariables'; import { localize } from '../../../localize'; import { isString } from '@microsoft/utils-logic-apps'; @@ -19,8 +20,8 @@ import type { WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; */ export function getGlobalSetting(key: string, prefix: string = ext.prefix): T | undefined { const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration(prefix); - const result: { globalValue?: T } | undefined = projectConfiguration.inspect(key); - return result && result.globalValue; + const result: { globalValue?: T; defaultValue?: T } | undefined = projectConfiguration.inspect(key); + return result && (result.globalValue || result.defaultValue); } /** @@ -88,7 +89,7 @@ function getScope(fsPath: WorkspaceFolder | string | undefined): Uri | Workspace } function osSupportsVersion(version: FuncVersion | undefined): boolean { - return version !== FuncVersion.v1 || process.platform === 'win32'; + return version !== FuncVersion.v1 || process.platform === Platform.windows; } /** diff --git a/apps/vs-code-designer/src/app/utils/vsCodeConfig/tasks.ts b/apps/vs-code-designer/src/app/utils/vsCodeConfig/tasks.ts index 3a25ec84b7c..a270d6c995d 100644 --- a/apps/vs-code-designer/src/app/utils/vsCodeConfig/tasks.ts +++ b/apps/vs-code-designer/src/app/utils/vsCodeConfig/tasks.ts @@ -2,9 +2,16 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { ITask, ITaskInputs } from '@microsoft/vscode-extension'; -import type { WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; +import { localize } from '../../../localize'; +import type { ProjectFile } from '../dotnet/dotnet'; +import { getDotnetDebugSubpath, getProjFiles, getTargetFramework } from '../dotnet/dotnet'; +import { tryGetFunctionProjectRoot } from '../verifyIsProject'; +import { DialogResponses, openUrl, type IActionContext } from '@microsoft/vscode-azext-utils'; +import { ProjectLanguage, type ITask, type ITaskInputs } from '@microsoft/vscode-extension'; +import * as fse from 'fs-extra'; +import * as path from 'path'; import { workspace } from 'vscode'; +import type { MessageItem, TaskDefinition, WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; const tasksKey = 'tasks'; const inputsKey = 'inputs'; @@ -72,3 +79,236 @@ export function updateTasksVersion(folder: WorkspaceFolder, version: string): vo function getTasksConfig(folder: WorkspaceFolder): WorkspaceConfiguration { return workspace.getConfiguration(tasksKey, folder.uri); } + +/** + * Validates the tasks.json is compatible with binary dependency use. + */ +export async function validateTasksJson(context: IActionContext, folders: ReadonlyArray | undefined): Promise { + context.telemetry.properties.lastStep = 'validateTasksJson'; + let overwrite = false; + if (folders) { + for (const folder of folders) { + const projectPath: string | undefined = await tryGetFunctionProjectRoot(context, folder); + context.telemetry.properties.projectPath = projectPath; + if (projectPath) { + const tasksJsonPath: string = path.join(projectPath, '.vscode', 'tasks.json'); + + if (!fse.existsSync(tasksJsonPath)) { + throw new Error(localize('noTaskJson', `Failed to find: ${tasksJsonPath}`)); + } + + const taskJsonData = fse.readFileSync(tasksJsonPath, 'utf-8'); + const taskJson = JSON.parse(taskJsonData); + const tasks: TaskDefinition[] = taskJson.tasks; + + if (tasks && Array.isArray(tasks)) { + tasks.forEach((task) => { + const command: string = task.command; + if (!command.startsWith('${config:azureLogicAppsStandard')) { + context.telemetry.properties.overwrite = 'true'; + overwrite = true; + } + }); + } + } + + if (overwrite) { + await overwriteTasksJson(context, projectPath); + } + } + } +} + +/** + * Overwrites the tasks.json file with the specified content. + * @param projectPath The project path. + **/ +async function overwriteTasksJson(context: IActionContext, projectPath: string): Promise { + if (projectPath) { + const message = + 'The Azure Logic Apps extension must update the tasks.json file to use the required and installed binary dependencies for Node JS, .NET Framework, and Azure Functions Core Tools. This update overwrites any custom-defined tasks you might have.' + + '\n\nSelecting "Cancel" leaves the file unchanged, but shows this message when you open this project again.' + + '\n\nContinue with the update?'; + + const tasksJsonPath: string = path.join(projectPath, '.vscode', 'tasks.json'); + let tasksJsonContent; + + const projectFiles = [ + ...(await getProjFiles(context, ProjectLanguage.CSharp, projectPath)), + ...(await getProjFiles(context, ProjectLanguage.FSharp, projectPath)), + ]; + if (projectFiles.length > 0) { + context.telemetry.properties.isNugetProj = 'true'; + const commonArgs: string[] = ['/property:GenerateFullPaths=true', '/consoleloggerparameters:NoSummary']; + const releaseArgs: string[] = ['--configuration', 'Release']; + + let projFile: ProjectFile; + const projFiles = [ + ...(await getProjFiles(context, ProjectLanguage.FSharp, projectPath)), + ...(await getProjFiles(context, ProjectLanguage.CSharp, projectPath)), + ]; + + if (projFiles.length === 1) { + projFile = projFiles[0]; + } else if (projFiles.length === 0) { + context.errorHandling.suppressReportIssue = true; + throw new Error( + localize('projNotFound', 'Failed to find {0} file in folder "{1}".', 'csproj or fsproj', path.basename(projectPath)) + ); + } else { + context.errorHandling.suppressReportIssue = true; + throw new Error( + localize( + 'projNotFound', + 'Expected to find a single {0} file in folder "{1}", but found multiple instead: {2}.', + `csproj or fsproj`, + path.basename(projectPath), + projFiles.join(', ') + ) + ); + } + + const targetFramework: string = await getTargetFramework(projFile); + const debugSubpath = getDotnetDebugSubpath(targetFramework); + tasksJsonContent = { + version: '2.0.0', + tasks: [ + { + label: 'generateDebugSymbols', + command: '${config:azureLogicAppsStandard.dotnetBinaryPath}', + args: ['${input:getDebugSymbolDll}'], + type: 'process', + problemMatcher: '$msCompile', + }, + { + label: 'clean', + command: '${config:azureLogicAppsStandard.dotnetBinaryPath}', + args: ['clean', ...commonArgs], + type: 'process', + problemMatcher: '$msCompile', + }, + { + label: 'build', + command: '${config:azureLogicAppsStandard.dotnetBinaryPath}', + args: ['build', ...commonArgs], + type: 'process', + dependsOn: 'clean', + group: { + kind: 'build', + isDefault: true, + }, + problemMatcher: '$msCompile', + }, + { + label: 'clean release', + command: '${config:azureLogicAppsStandard.dotnetBinaryPath}', + args: [...releaseArgs, ...commonArgs], + type: 'process', + problemMatcher: '$msCompile', + }, + { + label: 'publish', + command: '${config:azureLogicAppsStandard.dotnetBinaryPath}', + args: ['publish', ...releaseArgs, ...commonArgs], + type: 'process', + dependsOn: 'clean release', + problemMatcher: '$msCompile', + }, + { + label: 'func: host start', + dependsOn: 'build', + type: 'shell', + command: '${config:azureLogicAppsStandard.funcCoreToolsBinaryPath}', + args: ['host', 'start'], + options: { + cwd: debugSubpath, + env: { + PATH: '${config:azureLogicAppsStandard.autoRuntimeDependenciesPath}\\\\NodeJs;${config:azureLogicAppsStandard.autoRuntimeDependenciesPath}\\\\DotNetSDK;$env:PATH', + }, + }, + problemMatcher: '$func-watch', + isBackground: true, + }, + ], + inputs: [ + { + id: 'getDebugSymbolDll', + type: 'command', + command: 'azureLogicAppsStandard.getDebugSymbolDll', + }, + ], + }; + } else { + context.telemetry.properties.isNugetProj = 'false'; + tasksJsonContent = { + version: '2.0.0', + tasks: [ + { + label: 'generateDebugSymbols', + command: '${config:azureLogicAppsStandard.dotnetBinaryPath}', + args: ['${input:getDebugSymbolDll}'], + type: 'process', + problemMatcher: '$msCompile', + }, + { + type: 'shell', + command: '${config:azureLogicAppsStandard.funcCoreToolsBinaryPath}', + args: ['host', 'start'], + options: { + env: { + PATH: '${config:azureLogicAppsStandard.autoRuntimeDependenciesPath}\\\\NodeJs;${config:azureLogicAppsStandard.autoRuntimeDependenciesPath}\\\\DotNetSDK;$env:PATH', + }, + }, + problemMatcher: '$func-watch', + isBackground: true, + label: 'func: host start', + group: { + kind: 'build', + isDefault: true, + }, + }, + ], + inputs: [ + { + id: 'getDebugSymbolDll', + type: 'command', + command: 'azureLogicAppsStandard.getDebugSymbolDll', + }, + ], + }; + } + + // Add a "Don't warn again" option? + if (await confirmOverwriteFile(context, tasksJsonPath, message)) { + await fse.writeFile(tasksJsonPath, JSON.stringify(tasksJsonContent, null, 2)); + } + } +} + +/** + * Displays warning message to select if desire to overwrite file. + * @param {IActionContext} context - Command context. + * @param {string} fsPath - File path. + * @param {string} message - Message. + * @returns {Promise} True if user wants to overwrite file. + */ +async function confirmOverwriteFile(context: IActionContext, fsPath: string, message?: string): Promise { + if (await fse.pathExists(fsPath)) { + let result: MessageItem; + do { + result = await context.ui.showWarningMessage( + localize('fileAlreadyExists', message), + { modal: true }, + DialogResponses.yes, + DialogResponses.learnMore + ); + if (result === DialogResponses.learnMore) { + await openUrl('https://learn.microsoft.com/en-us/azure/logic-apps/create-single-tenant-workflows-visual-studio-code'); + } else if (result === DialogResponses.yes) { + return true; + } else { + return false; + } + } while (result === DialogResponses.learnMore); + } +} diff --git a/apps/vs-code-designer/src/app/utils/workspace.ts b/apps/vs-code-designer/src/app/utils/workspace.ts index 3d5f9eb4e99..47b40ec37d7 100644 --- a/apps/vs-code-designer/src/app/utils/workspace.ts +++ b/apps/vs-code-designer/src/app/utils/workspace.ts @@ -10,7 +10,7 @@ import { isNullOrUndefined, isString } from '@microsoft/utils-logic-apps'; import { UserCancelledError } from '@microsoft/vscode-azext-utils'; import type { IActionContext, IAzureQuickPickItem } from '@microsoft/vscode-azext-utils'; //TODO: revisit this import again (globby) -import globby from 'globby'; +import * as globby from 'globby'; import * as path from 'path'; import * as vscode from 'vscode'; diff --git a/apps/vs-code-designer/src/constants.ts b/apps/vs-code-designer/src/constants.ts index 63a0d4c33a0..d78cf85514f 100644 --- a/apps/vs-code-designer/src/constants.ts +++ b/apps/vs-code-designer/src/constants.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { localize } from './localize'; +import * as os from 'os'; +import * as path from 'path'; // File names export const hostFileName = 'host.json'; @@ -23,6 +25,11 @@ export const workflowDesignTimeDir = '/workflow-designtime'; export const logicAppsStandardExtensionId = 'ms-azuretools.vscode-azurelogicapps'; +// Azurite +export const azuriteExtensionId = 'Azurite.azurite'; +export const azuriteExtensionPrefix = 'azurite'; +export const azuriteLocationSetting = 'location'; + // Functions export const func = 'func'; export const functionsExtensionId = 'ms-azuretools.vscode-azurefunctions'; @@ -34,6 +41,16 @@ export const hostStartTaskName = `${func}: ${hostStartCommand}`; export const funcPackageName = 'azure-functions-core-tools'; export const defaultFuncPort = '7071'; export const isolatedSdkName = 'Microsoft.Azure.Functions.Worker.Sdk'; +export const funcDependencyName = 'FuncCoreTools'; + +// DotNet +export const dotnet = 'dotnet'; +export const dotnetDependencyName = 'DotNetSDK'; + +// Node +export const node = 'node'; +export const npm = 'npm'; +export const nodeJsDependencyName = 'NodeJs'; // Workflow export const workflowLocationKey = 'WORKFLOWS_LOCATION_NAME'; @@ -121,6 +138,8 @@ export enum extensionCommand { startRemoteDebug = 'azureLogicAppsStandard.startRemoteDebug', validateLogicAppProjects = 'azureLogicAppsStandard.validateFunctionProjects', reportIssue = 'azureLogicAppsStandard.reportIssue', + validateAndInstallBinaries = 'azureLogicAppsStandard.validateAndInstallBinaries', + azureAzuriteStart = 'azurite.start', loadDataMapFile = 'azureLogicAppsStandard.dataMap.loadDataMapFile', dataMapAddSchemaFromFile = 'azureLogicAppsStandard.dataMap.addSchemaFromFile', dataMapAttemptToResolveMissingSchemaFile = 'azureLogicAppsStandard.dataMap.attemptToResolveMissingSchemaFile', @@ -152,6 +171,8 @@ export const projectTemplateKeySetting = 'projectTemplateKey'; export const projectOpenBehaviorSetting = 'projectOpenBehavior'; export const stopFuncTaskPostDebugSetting = 'stopFuncTaskPostDebug'; export const validateFuncCoreToolsSetting = 'validateFuncCoreTools'; +export const validateDotNetSDKSetting = 'validateDotNetSDK'; +export const validateNodeJsSetting = 'validateNodeJs'; export const showDeployConfirmationSetting = 'showDeployConfirmation'; export const deploySubpathSetting = 'deploySubpath'; export const preDeployTaskSetting = 'preDeployTask'; @@ -159,6 +180,12 @@ export const pickProcessTimeoutSetting = 'pickProcessTimeout'; export const show64BitWarningSetting = 'show64BitWarning'; export const showProjectWarningSetting = 'showProjectWarning'; export const showTargetFrameworkWarningSetting = 'showTargetFrameworkWarning'; +export const showStartDesignTimeMessageSetting = 'showStartDesignTimeMessage'; +export const autoStartDesignTimeSetting = 'autoStartDesignTime'; +export const autoRuntimeDependenciesValidationAndInstallationSetting = 'autoRuntimeDependenciesValidationAndInstallation'; +export const azuriteBinariesLocationSetting = 'azuriteLocationSetting'; +export const showAutoStartAzuriteWarning = 'showAutoStartAzuriteWarning'; +export const autoStartAzuriteSetting = 'autoStartAzurite'; // Project export const defaultBundleId = 'Microsoft.Azure.Functions.ExtensionBundle'; @@ -169,12 +196,28 @@ export const extInstallTaskName = `${func}: ${extInstallCommand}`; export const tasksVersion = '2.0.0'; export const launchVersion = '0.2.0'; export const dotnetPublishTaskLabel = 'publish'; +export const autoRuntimeDependenciesPathSettingKey = 'autoRuntimeDependenciesPath'; +export const defaultLogicAppsFolder = '.azurelogicapps'; +export const defaultAzuritePathValue = path.join(os.homedir(), defaultLogicAppsFolder, '.azurite'); +export const defaultDependencyPathValue = path.join(os.homedir(), defaultLogicAppsFolder, 'dependencies'); +export const dotNetBinaryPathSettingKey = 'dotnetBinaryPath'; +export const nodeJsBinaryPathSettingKey = 'nodeJsBinaryPath'; +export const funcCoreToolsBinaryPathSettingKey = 'funcCoreToolsBinaryPath'; +export const dependencyTimeoutSettingKey = 'dependencyTimeout'; // local.settings.json export const localEmulatorConnectionString = 'UseDevelopmentStorage=true'; // host.json export const extensionBundleId = 'Microsoft.Azure.Functions.ExtensionBundle.Workflows'; +export const targetBundleKey = 'FUNCTIONS_EXTENSIONBUNDLE_SOURCE_URI'; + +// Fallback Dependency Versions +export enum DependencyVersion { + dotnet6 = '6.0.413', + funcCoreTools = '4.0.5198', + nodeJs = '18.17.1', +} export const hostFileContent = { version: '2.0', extensionBundle: { @@ -190,6 +233,12 @@ export const hostFileContent = { }, }; +export enum DependencyDefaultPath { + dotnet = 'dotnet', + funcCoreTools = 'func', + node = 'node', +} + // .NET export enum DotnetVersion { net6 = 'net6.0', @@ -205,6 +254,13 @@ export enum PackageManager { brew = 'brew', } +// Operating System Platforms +export enum Platform { + windows = 'win32', + mac = 'darwin', + linux = 'linux', +} + // Resources export const kubernetesKind = 'kubernetes'; export const functionAppKind = 'functionapp'; diff --git a/apps/vs-code-designer/src/extensionVariables.ts b/apps/vs-code-designer/src/extensionVariables.ts index c5459b7f358..ad8babda84f 100644 --- a/apps/vs-code-designer/src/extensionVariables.ts +++ b/apps/vs-code-designer/src/extensionVariables.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import type DataMapperPanel from './app/commands/dataMapper/DataMapperPanel'; import type { AzureAccountTreeItemWithProjects } from './app/tree/AzureAccountTreeItemWithProjects'; -import { func } from './constants'; +import { dotnet, func, node, npm } from './constants'; import type { Site } from '@azure/arm-appservice'; import type { IAzExtOutputChannel } from '@microsoft/vscode-azext-utils'; import type { AzureHostExtensionApi } from '@microsoft/vscode-azext-utils/hostapi'; @@ -22,6 +22,8 @@ export namespace ext { export let context: ExtensionContext; export let workflowDesignTimePort: number; export let workflowDesignChildProcess: cp.ChildProcess | undefined; + export let workflowDotNetProcess: cp.ChildProcess | undefined; + export let workflowNodeProcess: cp.ChildProcess | undefined; export let dataMapperRuntimePort: number; export let dataMapperChildProcess: cp.ChildProcess | undefined; export let logicAppWorkspace: string; @@ -45,6 +47,13 @@ export namespace ext { // Functions export const funcCliPath: string = func; + // DotNet + export const dotNetCliPath: string = dotnet; + + // Node Js + export const nodeJsCliPath: string = node; + export const npmCliPath: string = npm; + // WebViews export enum webViewKey { designerLocal = 'designerLocal', diff --git a/apps/vs-code-designer/src/main.ts b/apps/vs-code-designer/src/main.ts index 57dd99a01ab..c0fff28b88d 100644 --- a/apps/vs-code-designer/src/main.ts +++ b/apps/vs-code-designer/src/main.ts @@ -2,7 +2,6 @@ import { LogicAppResolver } from './LogicAppResolver'; import { runPostWorkflowCreateStepsFromCache } from './app/commands/createCodeless/createCodelessSteps/WorkflowCreateStepBase'; import { stopDataMapperBackend } from './app/commands/dataMapper/FxWorkflowRuntime'; import { supportedDataMapDefinitionFileExts, supportedSchemaFileExts } from './app/commands/dataMapper/extensionConfig'; -import { validateFuncCoreToolsIsLatest } from './app/commands/funcCoreTools/validateFuncCoreToolsIsLatest'; import { registerCommands } from './app/commands/registerCommands'; import { getResourceGroupsApi } from './app/resourcesExtension/getExtensionApi'; import type { AzureAccountTreeItemWithProjects } from './app/tree/AzureAccountTreeItemWithProjects'; @@ -13,6 +12,7 @@ import { registerFuncHostTaskEvents } from './app/utils/funcCoreTools/funcHostTa import { verifyVSCodeConfigOnActivate } from './app/utils/vsCodeConfig/verifyVSCodeConfigOnActivate'; import { extensionCommand, logicAppFilter } from './constants'; import { ext } from './extensionVariables'; +import { startOnboarding } from './onboarding'; import { registerAppServiceExtensionVariables } from '@microsoft/vscode-azext-azureappservice'; import { callWithTelemetryAndErrorHandling, @@ -54,7 +54,8 @@ export async function activate(context: vscode.ExtensionContext) { activateContext.telemetry.measurements.mainFileLoad = (perfStats.loadEndTime - perfStats.loadStartTime) / 1000; runPostWorkflowCreateStepsFromCache(); - validateFuncCoreToolsIsLatest(); + + await startOnboarding(activateContext); ext.extensionVersion = getExtensionVersion(); ext.rgApi = await getResourceGroupsApi(); @@ -62,10 +63,12 @@ export async function activate(context: vscode.ExtensionContext) { // @ts-ignore ext.azureAccountTreeItem = ext.rgApi.appResourceTree._rootTreeItem as AzureAccountTreeItemWithProjects; + activateContext.telemetry.properties.lastStep = 'verifyVSCodeConfigOnActivate'; callWithTelemetryAndErrorHandling(extensionCommand.validateLogicAppProjects, async (actionContext: IActionContext) => { await verifyVSCodeConfigOnActivate(actionContext, vscode.workspace.workspaceFolders); }); + activateContext.telemetry.properties.lastStep = 'registerEvent'; registerEvent( extensionCommand.validateLogicAppProjects, vscode.workspace.onDidChangeWorkspaceFolders, @@ -77,8 +80,11 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(ext.outputChannel); context.subscriptions.push(ext.azureAccountTreeItem); + activateContext.telemetry.properties.lastStep = 'registerReportIssueCommand'; registerReportIssueCommand(extensionCommand.reportIssue); + activateContext.telemetry.properties.lastStep = 'registerCommands'; registerCommands(); + activateContext.telemetry.properties.lastStep = 'registerFuncHostTaskEvents'; registerFuncHostTaskEvents(); ext.rgApi.registerApplicationResourceResolver(getAzExtResourceType(logicAppFilter), new LogicAppResolver()); diff --git a/apps/vs-code-designer/src/onboarding.ts b/apps/vs-code-designer/src/onboarding.ts new file mode 100644 index 00000000000..238a257b77c --- /dev/null +++ b/apps/vs-code-designer/src/onboarding.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { activateAzurite } from './app/utils/azurite/activateAzurite'; +import { promptInstallBinariesOption, validateAndInstallBinaries } from './app/utils/binaries'; +import { promptStartDesignTimeOption } from './app/utils/codeless/startDesignTimeApi'; +import { runWithDurationTelemetry } from './app/utils/telemetry'; +import { getGlobalSetting } from './app/utils/vsCodeConfig/settings'; +import { validateTasksJson } from './app/utils/vsCodeConfig/tasks'; +import { + extensionCommand, + autoRuntimeDependenciesValidationAndInstallationSetting, + autoStartDesignTimeSetting, + showStartDesignTimeMessageSetting, + showAutoStartAzuriteWarning, +} from './constants'; +import { callWithTelemetryAndErrorHandling, type IActionContext } from '@microsoft/vscode-azext-utils'; +import * as vscode from 'vscode'; + +/** + * Prompts warning message for installing the installing/validate binaries and taks.json. + * @param {IActionContext} activateContext - Activation context. + */ +export const onboardBinaries = async (activateContext: IActionContext) => { + callWithTelemetryAndErrorHandling(extensionCommand.validateAndInstallBinaries, async (actionContext: IActionContext) => { + await runWithDurationTelemetry(actionContext, extensionCommand.validateAndInstallBinaries, async () => { + const binariesInstallation = getGlobalSetting(autoRuntimeDependenciesValidationAndInstallationSetting); + if (binariesInstallation) { + activateContext.telemetry.properties.lastStep = extensionCommand.validateAndInstallBinaries; + await validateAndInstallBinaries(actionContext); + await validateTasksJson(actionContext, vscode.workspace.workspaceFolders); + } + }); + }); +}; + +/** + * Start onboarding experience prompting inputs for user. + * This function will propmpt/install dependencies binaries, start design time api and start azurite. + * @param {IActionContext} activateContext - Activation context. + */ +export const startOnboarding = async (activateContext: IActionContext) => { + callWithTelemetryAndErrorHandling(autoRuntimeDependenciesValidationAndInstallationSetting, async (actionContext: IActionContext) => { + await runWithDurationTelemetry(actionContext, autoRuntimeDependenciesValidationAndInstallationSetting, async () => { + activateContext.telemetry.properties.lastStep = autoRuntimeDependenciesValidationAndInstallationSetting; + await promptInstallBinariesOption(actionContext); + }); + }); + + await onboardBinaries(activateContext); + + await callWithTelemetryAndErrorHandling(autoStartDesignTimeSetting, async (actionContext: IActionContext) => { + await runWithDurationTelemetry(actionContext, showStartDesignTimeMessageSetting, async () => { + await promptStartDesignTimeOption(activateContext); + }); + }); + + await callWithTelemetryAndErrorHandling(showAutoStartAzuriteWarning, async (actionContext: IActionContext) => { + await runWithDurationTelemetry(actionContext, showAutoStartAzuriteWarning, async () => { + activateContext.telemetry.properties.lastStep = showAutoStartAzuriteWarning; + activateAzurite(activateContext); + }); + }); +}; diff --git a/apps/vs-code-designer/src/package.json b/apps/vs-code-designer/src/package.json index 5042bae1740..0098ae45547 100644 --- a/apps/vs-code-designer/src/package.json +++ b/apps/vs-code-designer/src/package.json @@ -303,7 +303,13 @@ }, { "command": "azureLogicAppsStandard.reportIssue", - "title": "Report Issue...", + "title": "Report issue...", + "category": "Azure Logic Apps" + }, + { + "command": "azureLogicAppsStandard.validateAndInstallBinaries", + "title": "Validate and install dependency binaries", + "preview": true, "category": "Azure Logic Apps" }, { @@ -729,7 +735,35 @@ "azureLogicAppsStandard.deploySubpath": { "scope": "resource", "type": "string", - "description": "The default subpath of a workspace folder to use when deploying. If set, you will not be prompted for the folder path when deploying." + "description": "The default subpath for the workspace folder to use during deployment. If you set this value, you won't get a prompt for the folder path during deployment." + }, + "azureLogicAppsStandard.dependencyTimeout": { + "type": "number", + "description": "The timeout (in seconds) to be used when validating and installing dependencies.", + "default": 120 + }, + "azureLogicAppsStandard.autoRuntimeDependenciesPath": { + "scope": "resource", + "type": "string", + "description": "The path for Azure Logic Apps extension runtime dependencies. (Preview)" + }, + "azureLogicAppsStandard.dotnetBinaryPath": { + "scope": "resource", + "type": "string", + "description": "The path for Azure Logic Apps extension .NET SDK dependency binary.", + "default": "dotnet" + }, + "azureLogicAppsStandard.nodeJsBinaryPath": { + "scope": "resource", + "type": "string", + "description": "The path for Azure Logic Apps extension Node JS dependency binary.", + "default": "node" + }, + "azureLogicAppsStandard.funcCoreToolsBinaryPath": { + "scope": "resource", + "type": "string", + "description": "The path for Azure Logic Apps extension Azure Function Core Tools dependency binary.", + "default": "func" }, "azureLogicAppsStandard.projectSubpath": { "scope": "resource", @@ -748,22 +782,42 @@ }, "azureLogicAppsStandard.validateFuncCoreTools": { "type": "boolean", - "description": "Validate the Azure Functions Core Tools is installed before debugging.", + "description": "Make sure that Azure Functions Core Tools is installed before you start debugging.", + "default": true + }, + "azureLogicAppsStandard.validateDotNetSDK": { + "type": "boolean", + "description": "Make sure that the .NET SDK is installed before you start debugging.", + "default": true + }, + "azureLogicAppsStandard.validateNodeJs": { + "type": "boolean", + "description": "Make sure that Node JS is installed before you start debugging.", "default": true }, "azureLogicAppsStandard.showDeployConfirmation": { "type": "boolean", - "description": "Ask for confirmation before deploying to a Function App in Azure (deploying will overwrite any previous deployment and cannot be undone).", + "description": "Ask to confirm before deploying to a function app in Azure. Deployment overwrites any previous deployment and can't be undone.", "default": true }, "azureLogicAppsStandard.showCoreToolsWarning": { "type": "boolean", - "description": "Show a warning if your installed version of Azure Functions Core Tools is out-of-date.", + "description": "Show a warning when your installed version of the Azure Functions Core Tools is outdated.", + "default": true + }, + "azureLogicAppsStandard.showDotNetWarning": { + "type": "boolean", + "description": "Show a warning when your installed version of the .NET SDK is outdated.", + "default": true + }, + "azureLogicAppsStandard.showNodeJsWarning": { + "type": "boolean", + "description": "Show a warning when your installed version of Node JS is outdated.", "default": true }, "azureLogicAppsStandard.showMultiCoreToolsWarning": { "type": "boolean", - "description": "Show a warning if multiple installs of Azure Functions Core Tools are detected.", + "description": "Show a warning when multiple installations of the Azure Functions Core Tools are found.", "default": true }, "azureLogicAppsStandard.requestTimeout": { @@ -812,9 +866,38 @@ }, "azureLogicAppsStandard.showTargetFrameworkWarning": { "type": "boolean", - "description": "Show a warning when an Azure Functions .NET project was detected that has mismatched target frameworks.", + "description": "Show a warning after detecting an Azure Functions .NET project with mismatched target frameworks.", + "default": true + }, + "azureLogicAppsStandard.showStartDesignTimeMessage": { + "type": "boolean", + "description": "Show a message asking customers if they want to start the background design-time process at project load time.", "default": true }, + "azureLogicAppsStandard.autoStartDesignTime": { + "type": "boolean", + "description": "Start background design-time process at project load time.", + "default": true + }, + "azureLogicAppsStandard.autoRuntimeDependenciesValidationAndInstallation": { + "type": "boolean", + "description": "Enable automatic validation and installation for runtime dependencies at the configured path. (Preview)", + "default": null + }, + "azureLogicAppsStandard.showAutoStartAzuriteWarning": { + "type": "boolean", + "description": "Show a warning asking if user's would like to configure Azurite auto start.", + "default": false + }, + "azureLogicAppsStandard.autoStartAzurite": { + "type": "boolean", + "description": "Start Azurite when project starts.", + "default": true + }, + "azureLogicAppsStandard.azuriteLocationSetting": { + "type": "string", + "description": "Default location for Azurite repository for Logic Apps Standard Workspaces" + }, "azureLogicAppsStandard.useExpandedFunctionCards": { "type": "boolean", "default": true, @@ -880,7 +963,8 @@ "onView:azDataMapper", "workspaceContains:host.json", "workspaceContains:*/host.json", - "onDebugInitialConfigurations" + "onDebugInitialConfigurations", + "onStartupFinished" ], "galleryBanner": { "color": "#015cda", diff --git a/apps/vs-code-designer/tsconfig.json b/apps/vs-code-designer/tsconfig.json index 0d3d78fe677..aec65d3ea30 100644 --- a/apps/vs-code-designer/tsconfig.json +++ b/apps/vs-code-designer/tsconfig.json @@ -3,8 +3,7 @@ "compilerOptions": { "jsx": "react-jsx", "allowSyntheticDefaultImports": true, - "resolveJsonModule": true, - "esModuleInterop": true + "resolveJsonModule": true }, "files": [ "../../node_modules/@nrwl/react/typings/cssmodule.d.ts", diff --git a/libs/vscode-extension/src/lib/models/artifact.ts b/libs/vscode-extension/src/lib/models/artifact.ts index 0630441a26d..1a122075a75 100644 --- a/libs/vscode-extension/src/lib/models/artifact.ts +++ b/libs/vscode-extension/src/lib/models/artifact.ts @@ -13,3 +13,23 @@ export interface FileDetails { fileName: string; relativePath: string; } + +/** + * Describes the release info object response from https://api.github.com/repos/OWNER/REPO/releases + */ +export interface IGitHubReleaseInfo { + /** + * The version + */ + tag_name?: string; + + /** + * Name of release (includes version) + */ + name?: string; + body?: string; + url?: string; + assets_url?: string; + upload_url?: string; + id?: number; +} diff --git a/libs/vscode-extension/src/lib/models/bundleFeed.ts b/libs/vscode-extension/src/lib/models/bundleFeed.ts index f051c3b2317..7a83691f81e 100644 --- a/libs/vscode-extension/src/lib/models/bundleFeed.ts +++ b/libs/vscode-extension/src/lib/models/bundleFeed.ts @@ -13,3 +13,9 @@ export interface IBundleFeed { }; }; } + +export interface IBundleDependencyFeed { + dotnet?: string; + funcCoreTools?: string; + nodejs?: string; +} diff --git a/package-lock.json b/package-lock.json index 766167fd0c1..1328fbd923a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@microsoft/logicappsux", - "version": "2.77.0", + "version": "2.78.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@microsoft/logicappsux", - "version": "2.77.0", + "version": "2.78.0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -37,8 +37,10 @@ "@react-hookz/web": "22.0.0", "@reduxjs/toolkit": "1.8.5", "@swc/helpers": "~0.3.3", + "@types/adm-zip": "^0.5.1", "@types/lodash.isequal": "^4.5.6", "@types/pathfinding": "^0.0.6", + "@types/request": "^2.48.8", "@use-it/event-listener": "0.1.7", "axios": "^1.3.4", "axios-retry": "^3.5.0", @@ -9735,6 +9737,14 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, + "node_modules/@types/adm-zip": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.1.tgz", + "integrity": "sha512-3+psmbh60N5JXM2LMkujFqnjMf3KB0LZoIQO73NJvkv57q+384nK/A7pP0v+ZkB/Zrfqn+5xtAyt5OsY+GiYLQ==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/aria-query": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz", @@ -9808,6 +9818,11 @@ "@types/node": "*" } }, + "node_modules/@types/caseless": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.3.tgz", + "integrity": "sha512-ZD/NsIJYq/2RH+hY7lXmstfp/v9djGt9ah+xRQ3pcgR79qiKsG4pLl25AI7IcXxVO8dH9GiBE5rAknC0ePntlw==" + }, "node_modules/@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -10515,6 +10530,30 @@ "@types/react": "*" } }, + "node_modules/@types/request": { + "version": "2.48.8", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.8.tgz", + "integrity": "sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ==", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/request/node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -40331,6 +40370,14 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, + "@types/adm-zip": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.1.tgz", + "integrity": "sha512-3+psmbh60N5JXM2LMkujFqnjMf3KB0LZoIQO73NJvkv57q+384nK/A7pP0v+ZkB/Zrfqn+5xtAyt5OsY+GiYLQ==", + "requires": { + "@types/node": "*" + } + }, "@types/aria-query": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz", @@ -40404,6 +40451,11 @@ "@types/node": "*" } }, + "@types/caseless": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.3.tgz", + "integrity": "sha512-ZD/NsIJYq/2RH+hY7lXmstfp/v9djGt9ah+xRQ3pcgR79qiKsG4pLl25AI7IcXxVO8dH9GiBE5rAknC0ePntlw==" + }, "@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -41095,6 +41147,29 @@ "@types/react": "*" } }, + "@types/request": { + "version": "2.48.8", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.8.tgz", + "integrity": "sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ==", + "requires": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + }, + "dependencies": { + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + } + } + }, "@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", diff --git a/package.json b/package.json index 9815257144a..ded126279c9 100644 --- a/package.json +++ b/package.json @@ -71,8 +71,10 @@ "@react-hookz/web": "22.0.0", "@reduxjs/toolkit": "1.8.5", "@swc/helpers": "~0.3.3", + "@types/adm-zip": "^0.5.1", "@types/lodash.isequal": "^4.5.6", "@types/pathfinding": "^0.0.6", + "@types/request": "^2.48.8", "@use-it/event-listener": "0.1.7", "axios": "^1.3.4", "axios-retry": "^3.5.0",