diff --git a/.github/workflows/pr-coverage.yml b/.github/workflows/pr-coverage.yml index c04be0f3dc4..48790202450 100644 --- a/.github/workflows/pr-coverage.yml +++ b/.github/workflows/pr-coverage.yml @@ -169,12 +169,8 @@ jobs: apps/vs-code-designer/src/app/utils/vsCodeConfig/tasks.ts apps/vs-code-designer/src/app/debug/validatePreDebug.ts apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/CreateLogicAppVSCodeContents.ts - apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/initCustomCodeProjectStep.ts - apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/initCustomCodeProjectStepBase.ts - apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/initCustomCodeScriptProjectStep.ts apps/vs-code-designer/src/app/commands/initProjectForVSCode/initDotnetProjectStep.ts apps/vs-code-designer/src/app/commands/initProjectForVSCode/initProjectStep.ts - apps/vs-code-designer/src/app/commands/initProjectForVSCode/initScriptProjectStep.ts - name: Check coverage on changed files id: coverage-check diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/CreateFunctionAppFiles.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/CreateFunctionAppFiles.ts index c4b125a5f37..d083559e2df 100644 --- a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/CreateFunctionAppFiles.ts +++ b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/CreateFunctionAppFiles.ts @@ -7,30 +7,20 @@ import { functionsExtensionId, vscodeFolderName, extensionsFileName, - launchFileName, settingsFileName, tasksFileName, - extensionCommand, } from '../../../../constants'; -import { FuncVersion, type IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps'; +import { type IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps'; import { TargetFramework, ProjectType } from '@microsoft/vscode-extension-logic-apps'; import * as fs from 'fs-extra'; import * as path from 'path'; import { getAssetsRoot } from '../../../utils/assets'; -import { getDebugConfigs, updateDebugConfigs } from '../../../utils/vsCodeConfig/launch'; -import { getContainingWorkspace, isMultiRootWorkspace } from '../../../utils/workspace'; -import { localize } from '../../../../localize'; -import * as vscode from 'vscode'; -import { getCustomCodeRuntime } from '../../../utils/debug'; import { createCsFile, createProgramFile, createRulesFiles, createCsprojFile } from '../../../utils/functionProjectFiles'; /** * This class represents a prompt step that allows the user to set up an Azure Function project. */ export class CreateFunctionAppFiles { - // Hide the step count in the wizard UI - public hideStepCount = true; - /** * Sets up an Azure Function project by creating all necessary files. * @param context The project wizard context. @@ -85,91 +75,6 @@ export class CreateFunctionAppFiles { await fs.writeJson(filePath, content, { spaces: 2 }); } - /** - * Updates the launch.json file for the logic app corresponding to this functions app. - * @param folderPath The functions app folder path. - * @param targetFramework The target framework of the functions app. - * @param funcVersion The version of the functions app. - * @param logicAppName The name of the logic app. - */ - private async updateLogicAppLaunchJson( - folderPath: string, - targetFramework: TargetFramework, - funcVersion: FuncVersion, - logicAppName: string - ): Promise { - const logicAppLaunchJsonPath = path.join(folderPath, '..', '..', logicAppName, vscodeFolderName, launchFileName); - let logicAppWorkspaceFolder = getContainingWorkspace(logicAppLaunchJsonPath); - if (logicAppWorkspaceFolder === undefined) { - // Traverse up the directory tree to find a .code-workspace file - let currentPath = path.dirname(logicAppLaunchJsonPath); - let workspaceFile: string | undefined; - - while (currentPath !== path.parse(currentPath).root) { - const files = await fs.readdir(currentPath); - const codeWorkspaceFile = files.find((file) => file.endsWith('.code-workspace')); - - if (codeWorkspaceFile) { - workspaceFile = path.join(currentPath, codeWorkspaceFile); - break; - } - - currentPath = path.dirname(currentPath); - } - - // If no workspace file found, cannot update launch.json - if (workspaceFile) { - logicAppWorkspaceFolder = { - uri: vscode.Uri.file(currentPath), - name: path.basename(currentPath), - index: 0, - }; - } - } - const debugConfigs = getDebugConfigs(logicAppWorkspaceFolder); - const updatedDebugConfigs = debugConfigs.some((debugConfig) => debugConfig.type === 'logicapp') - ? debugConfigs.map((debugConfig) => { - // Update the logic app debug configuration to use the correct runtime for custom code - if (debugConfig.type === 'logicapp') { - return { - ...debugConfig, - customCodeRuntime: getCustomCodeRuntime(targetFramework), - isCodeless: true, - }; - } - return debugConfig; - }) - : [ - { - name: localize('debugLogicApp', `Run/Debug logic app with local function ${logicAppName}`), - type: 'logicapp', - request: 'launch', - funcRuntime: funcVersion === FuncVersion.v1 ? 'clr' : 'coreclr', - customCodeRuntime: getCustomCodeRuntime(targetFramework), - isCodeless: true, - }, - ...debugConfigs.filter( - (debugConfig) => debugConfig.request !== 'attach' || debugConfig.processId !== `\${command:${extensionCommand.pickProcess}}` - ), - ]; - - if (isMultiRootWorkspace()) { - let launchJsonContent: any; - if (await fs.pathExists(logicAppLaunchJsonPath)) { - launchJsonContent = await fs.readJson(logicAppLaunchJsonPath); - launchJsonContent['configurations'] = updatedDebugConfigs; - } else { - launchJsonContent = { - version: '0.2.0', - configurations: updatedDebugConfigs, - }; - } - await fs.writeJson(logicAppLaunchJsonPath, launchJsonContent, { spaces: 2 }); - } else { - updateDebugConfigs(logicAppWorkspaceFolder, updatedDebugConfigs); - } - } - /** * Generates the settings.json file in the specified folder. * @param folderPath The path to the folder where the settings.json file should be generated. diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/CreateLogicAppVSCodeContents.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/CreateLogicAppVSCodeContents.ts index 060bf2309a6..b6ff22ca7d7 100644 --- a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/CreateLogicAppVSCodeContents.ts +++ b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/CreateLogicAppVSCodeContents.ts @@ -1,258 +1,97 @@ -import { latestGAVersion, ProjectLanguage, ProjectType, TargetFramework } from '@microsoft/vscode-extension-logic-apps'; -import type { ILaunchJson, ISettingToAdd, IWebviewProjectContext } from '@microsoft/vscode-extension-logic-apps'; +import { ProjectType, ProjectPackageType, type IWebviewProjectContext } from '@microsoft/vscode-extension-logic-apps'; import { - deploySubpathSetting, - dotnetExtensionId, - dotnetPublishTaskLabel, devContainerFileName, devContainerFolderName, - extensionCommand, extensionsFileName, - func, funcDependencyName, - funcVersionSetting, - funcWatchProblemMatcher, - hostStartCommand, launchFileName, - launchVersion, - projectLanguageSetting, settingsFileName, tasksFileName, vscodeFolderName, } from '../../../../constants'; import path from 'path'; import * as fse from 'fs-extra'; -import type { DebugConfiguration } from 'vscode'; -import { getContainerTemplatePath, getWorkspaceTemplatePath } from '../../../utils/assets'; -import { getFuncHostTaskEnv } from '../../../utils/codeless/funcHostTaskEnv'; -import { confirmEditJsonFile } from '../../../utils/fs'; -import { localize } from '../../../../localize'; -import { ext } from '../../../../extensionVariables'; -import { getCustomCodeRuntime } from '../../../utils/debug'; -import { isDebugConfigEqual } from '../../../utils/vsCodeConfig/launch'; +import { getContainerTemplatePath } from '../../../utils/assets'; import { binariesExistSync } from '../../../utils/binaries'; -import { tryGetLogicAppProjectRoot } from '../../../utils/verifyIsProject'; import { - type CustomCodeFunctionsProjectMetadata, - getCustomCodeFunctionsProjectMetadata, - tryGetLogicAppCustomCodeFunctionsProjects, -} from '../../../utils/customCodeUtils'; + generateTasksJson, + generateLaunchJson, + generateSettingsJson, + generateExtensionsJson, +} from '../../../utils/vsCodeConfig/generators'; +import { detectCustomCodeTargetFramework } from '../../../utils/customCodeUtils'; -export async function writeSettingsJson( - context: IWebviewProjectContext, - additionalSettings: ISettingToAdd[], - vscodePath: string -): Promise { - const { targetFramework, logicAppType } = context; +export async function createLogicAppVsCodeContents(webviewProjectContext: IWebviewProjectContext, logicAppFolderPath: string) { + const { logicAppName } = webviewProjectContext; + const vscodePath: string = path.join(logicAppFolderPath, vscodeFolderName); + await fse.ensureDir(vscodePath); - const settings: ISettingToAdd[] = [ - ...additionalSettings, - { key: projectLanguageSetting, value: logicAppType === ProjectType.codeful ? ProjectLanguage.CSharp : ProjectLanguage.JavaScript }, - { key: funcVersionSetting, value: latestGAVersion }, - // We want the terminal to open after F5, not the debug console because HTTP triggers are printed in the terminal. - { prefix: 'debug', key: 'internalConsoleOptions', value: 'neverOpen' }, - { prefix: 'azureFunctions', key: 'suppressProject', value: true }, - ]; + await writeSettingsJson(webviewProjectContext, vscodePath); + await writeExtensionsJson(webviewProjectContext, vscodePath); + await writeTasksJson(webviewProjectContext, vscodePath); + await writeLaunchJson(webviewProjectContext, vscodePath, logicAppFolderPath, logicAppName); +} - const settingsJsonPath: string = path.join(vscodePath, settingsFileName); +export async function writeSettingsJson(context: IWebviewProjectContext, vscodePath: string): Promise { + const { targetFramework, logicAppType } = context; - if (logicAppType === ProjectType.codeful) { - const deploySubPathValue = path.posix.join('bin', 'Release', targetFramework ?? TargetFramework.NetFx, 'publish'); - settings.push( - { prefix: 'azureFunctions', key: 'deploySubpath', value: deploySubPathValue }, - { prefix: 'azureFunctions', key: 'preDeployTask', value: 'publish' }, - { prefix: 'azureFunctions', key: 'projectSubpath', value: deploySubPathValue }, - // Prevent OmniSharp from generating invalid solution files - { prefix: 'omnisharp', key: 'enableMsBuildLoadProjectsOnDemand', value: false }, - { prefix: 'omnisharp', key: 'disableMSBuildDiagnosticWarning', value: true } - ); - } - await confirmEditJsonFile(context, settingsJsonPath, (data: Record): Record => { - for (const setting of settings) { - const key = `${setting.prefix || ext.prefix}.${setting.key}`; - data[key] = setting.value; - } - return data; + const projectPackageType = logicAppType === ProjectType.codeful ? ProjectPackageType.Nuget : ProjectPackageType.Bundle; + const settingsContent = generateSettingsJson({ + projectType: logicAppType, + projectPackageType, + hasFuncBinaries: binariesExistSync(funcDependencyName), + targetFramework, }); + + const settingsJsonPath: string = path.join(vscodePath, settingsFileName); + await fse.writeJson(settingsJsonPath, settingsContent, { spaces: 2 }); } export async function writeExtensionsJson(webviewProjectContext: IWebviewProjectContext, vscodePath: string): Promise { - const { logicAppType } = webviewProjectContext; const extensionsJsonPath: string = path.join(vscodePath, extensionsFileName); - const extensionsJsonFile = 'ExtensionsJsonFile'; - const templatePath = getWorkspaceTemplatePath(extensionsJsonFile); - const templateContent = await fse.readFile(templatePath, 'utf-8'); - const extensionsData = JSON.parse(templateContent); - - if (logicAppType !== ProjectType.logicApp) { - extensionsData.recommendations = [...(extensionsData.recommendations || []), ...[dotnetExtensionId]]; - } + const extensionsData = generateExtensionsJson(); await fse.writeJson(extensionsJsonPath, extensionsData, { spaces: 2 }); } -const getCodefulTasks = (targetFramework: string) => { - const commonDotnetArgs: string[] = ['/property:GenerateFullPaths=true', '/consoleloggerparameters:NoSummary']; - const releaseDotnetArgs: string[] = ['--configuration', 'Release']; - const funcBinariesExist = binariesExistSync(funcDependencyName); - const debugSubpath = path.posix.join('bin', 'Debug', targetFramework); - const binariesOptions = funcBinariesExist ? getFuncHostTaskEnv({ cwd: debugSubpath }) : {}; - return [ - { - label: 'clean', - command: '${config:azureLogicAppsStandard.dotnetBinaryPath}', - args: ['clean', ...commonDotnetArgs], - type: 'process', - problemMatcher: '$msCompile', - }, - { - label: 'build', - command: '${config:azureLogicAppsStandard.dotnetBinaryPath}', - args: ['build', ...commonDotnetArgs], - type: 'process', - dependsOn: 'clean', - group: { - kind: 'build', - isDefault: true, - }, - problemMatcher: '$msCompile', - }, - { - label: 'clean release', - command: '${config:azureLogicAppsStandard.dotnetBinaryPath}', - args: ['clean', ...releaseDotnetArgs, ...commonDotnetArgs], - type: 'process', - problemMatcher: '$msCompile', - }, - { - label: dotnetPublishTaskLabel, - command: '${config:azureLogicAppsStandard.dotnetBinaryPath}', - args: ['publish', ...releaseDotnetArgs, ...commonDotnetArgs], - type: 'process', - dependsOn: 'clean release', - problemMatcher: '$msCompile', - }, - { - label: 'func: host start', - type: funcBinariesExist ? 'shell' : func, - dependsOn: 'build', - ...binariesOptions, - command: funcBinariesExist ? '${config:azureLogicAppsStandard.funcCoreToolsBinaryPath}' : hostStartCommand, - args: funcBinariesExist ? ['host', 'start'] : undefined, - isBackground: true, - problemMatcher: funcWatchProblemMatcher, - }, - ]; -}; - export async function writeTasksJson(context: IWebviewProjectContext, vscodePath: string): Promise { const { targetFramework, logicAppType } = context; const tasksJsonPath: string = path.join(vscodePath, tasksFileName); - const tasksJsonFile = context.isDevContainerProject ? 'DevContainerTasksJsonFile' : 'TasksJsonFile'; - const templatePath = getWorkspaceTemplatePath(tasksJsonFile); - const templateContent = await fse.readFile(templatePath, 'utf-8'); - const tasksData = JSON.parse(templateContent); - - if (logicAppType === ProjectType.codeful && targetFramework) { - const codefulTasks = getCodefulTasks(targetFramework); - tasksData.tasks = codefulTasks; - - await fse.writeJson(tasksJsonPath, tasksData, { spaces: 2 }); - } else { - await fse.copyFile(templatePath, tasksJsonPath); - } -} -export async function writeDevContainerJson(devContainerPath: string): Promise { - const devContainerJsonPath: string = path.join(devContainerPath, devContainerFileName); - const templatePath = getContainerTemplatePath(devContainerFileName); - await fse.copyFile(templatePath, devContainerJsonPath); -} - -export function getDebugConfiguration( - logicAppName: string, - customCodeTargetFramework?: TargetFramework, - isCodeful?: boolean -): DebugConfiguration { - // NOTE(aeldridge): Only use logicapp debug configuration for custom code and codeful projects for now. Simple attach is sufficient for codeless non-custom code. - if (isCodeful) { - return { - name: localize('debugLogicApp', `Run/Debug logic app ${logicAppName}`), - type: 'logicapp', - request: 'launch', - funcRuntime: 'coreclr', - isCodeless: false, - }; - } + const projectPackageType = logicAppType === ProjectType.codeful ? ProjectPackageType.Nuget : ProjectPackageType.Bundle; - if (customCodeTargetFramework) { - return { - name: localize('debugLogicApp', `Run/Debug logic app with local function ${logicAppName}`), - type: 'logicapp', - request: 'launch', - funcRuntime: 'coreclr', - customCodeRuntime: getCustomCodeRuntime(customCodeTargetFramework), - isCodeless: true, - }; - } + const tasksJsonContent = generateTasksJson({ + projectType: logicAppType, + projectPackageType: projectPackageType, + hasFuncBinaries: binariesExistSync(funcDependencyName), + targetFramework, + isDevContainer: context.isDevContainerProject, + }); - return { - name: localize('attachToNetFunc', `Run/Debug logic app ${logicAppName}`), - type: 'coreclr', - request: 'attach', - processId: `\${command:${extensionCommand.pickProcess}}`, - }; + await fse.writeJson(tasksJsonPath, tasksJsonContent, { spaces: 2 }); } -export async function writeLaunchJson(context: IWebviewProjectContext, vscodePath: string, logicAppName: string): Promise { +export async function writeLaunchJson( + context: IWebviewProjectContext, + vscodePath: string, + logicAppFolderPath: string, + logicAppName: string +): Promise { const customCodeTargetFramework = context.logicAppType === ProjectType.customCode || context.logicAppType === ProjectType.rulesEngine - ? (context.targetFramework ?? (await tryGetCustomCodeTargetFramework(context))) + ? (context.targetFramework ?? (await detectCustomCodeTargetFramework(logicAppFolderPath))) : undefined; - const isCodeful = context.logicAppType === ProjectType.codeful; - const newDebugConfig: DebugConfiguration = getDebugConfiguration(logicAppName, customCodeTargetFramework, isCodeful); - // otherwise manually edit json - const launchJsonPath: string = path.join(vscodePath, launchFileName); - await confirmEditJsonFile(context, launchJsonPath, (data: ILaunchJson): ILaunchJson => { - data.version = launchVersion; - data.configurations = insertLaunchConfig(data.configurations, newDebugConfig); - return data; + const launchContent = generateLaunchJson({ + projectType: context.logicAppType, + projectPackageType: context.logicAppType === ProjectType.codeful ? ProjectPackageType.Nuget : ProjectPackageType.Bundle, + hasFuncBinaries: binariesExistSync(funcDependencyName), + customCodeTargetFramework, + logicAppName, }); -} - -async function tryGetCustomCodeTargetFramework(context: IWebviewProjectContext): Promise { - const workspaceFolder = path.join(context.workspaceProjectPath.fsPath, context.workspaceName); - const logicAppFolderPath = await tryGetLogicAppProjectRoot(context, workspaceFolder); - const customCodeProjectPaths = await tryGetLogicAppCustomCodeFunctionsProjects(logicAppFolderPath); - let customCodeProjectsMetadata: CustomCodeFunctionsProjectMetadata[]; - if (customCodeProjectPaths && customCodeProjectPaths.length > 0) { - customCodeProjectsMetadata = await Promise.all(customCodeProjectPaths.map(getCustomCodeFunctionsProjectMetadata)); - } - // Currently only support one custom code functions project per logic app - return customCodeProjectsMetadata ? customCodeProjectsMetadata[0].targetFramework : undefined; -} - -export function insertLaunchConfig(existingConfigs: DebugConfiguration[] | undefined, newConfig: DebugConfiguration): DebugConfiguration[] { - const configs = (existingConfigs ?? []).filter((existingConfig) => !isDebugConfigEqual(existingConfig, newConfig)); - return [...configs, newConfig]; -} -export async function createLogicAppVsCodeContents(webviewProjectContext: IWebviewProjectContext, logicAppFolderPath: string) { - const { logicAppType, logicAppName } = webviewProjectContext; - const vscodePath: string = path.join(logicAppFolderPath, vscodeFolderName); - await fse.ensureDir(vscodePath); - - const additionalSettings: ISettingToAdd[] = []; - - if (logicAppType === ProjectType.logicApp) { - additionalSettings.push({ key: deploySubpathSetting, value: '.' }); - } - - await writeSettingsJson(webviewProjectContext, additionalSettings, vscodePath); - await writeExtensionsJson(webviewProjectContext, vscodePath); - await writeTasksJson(webviewProjectContext, vscodePath); - await writeLaunchJson(webviewProjectContext, vscodePath, logicAppName); + const launchJsonPath: string = path.join(vscodePath, launchFileName); + await fse.writeJson(launchJsonPath, launchContent, { spaces: 2 }); } export async function createDevContainerContents(webviewProjectContext: IWebviewProjectContext, workspaceFolder: string): Promise { @@ -262,3 +101,9 @@ export async function createDevContainerContents(webviewProjectContext: IWebview await writeDevContainerJson(devContainerPath); } } + +export async function writeDevContainerJson(devContainerPath: string): Promise { + const devContainerJsonPath: string = path.join(devContainerPath, devContainerFileName); + const templatePath = getContainerTemplatePath(devContainerFileName); + await fse.copyFile(templatePath, devContainerJsonPath); +} diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/__test__/CreateLogicAppVSCodeContents.test.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/__test__/CreateLogicAppVSCodeContents.test.ts index c0119ab8846..0f57b8d756c 100644 --- a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/__test__/CreateLogicAppVSCodeContents.test.ts +++ b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/__test__/CreateLogicAppVSCodeContents.test.ts @@ -1,17 +1,15 @@ -import { describe, it, expect, vi, beforeAll, beforeEach, afterEach, type Mock } from 'vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import * as CreateLogicAppVSCodeContentsModule from '../CreateLogicAppVSCodeContents'; import * as fse from 'fs-extra'; import * as path from 'path'; import * as fsUtils from '../../../../utils/fs'; import { ProjectType, TargetFramework } from '@microsoft/vscode-extension-logic-apps'; import type { IWebviewProjectContext } from '@microsoft/vscode-extension-logic-apps'; -import { assetsFolderName, workspaceTemplatesFolderName } from '../../../../../constants'; vi.mock('fs-extra', () => ({ ensureDir: vi.fn(), copyFile: vi.fn(), pathExists: vi.fn(), - readFile: vi.fn(), writeJson: vi.fn(), })); vi.mock('../../../../utils/fs', () => ({ @@ -66,19 +64,6 @@ describe('CreateLogicAppVSCodeContents', () => { const logicAppFolderPath = path.join('test', 'workspace', 'TestLogicApp'); - let extensionsJsonFileContent: string; - let tasksJsonFileContent: string; - let devContainerTasksJsonFileContent: string; - - beforeAll(async () => { - const realFs = await vi.importActual('fs-extra'); - const templatesFolderPath = path.join(__dirname, '..', '..', '..', '..', '..', assetsFolderName, workspaceTemplatesFolderName); - - extensionsJsonFileContent = await realFs.readFile(path.join(templatesFolderPath, 'ExtensionsJsonFile'), 'utf8'); - tasksJsonFileContent = await realFs.readFile(path.join(templatesFolderPath, 'TasksJsonFile'), 'utf8'); - devContainerTasksJsonFileContent = await realFs.readFile(path.join(templatesFolderPath, 'DevContainerTasksJsonFile'), 'utf8'); - }); - beforeEach(() => { vi.resetAllMocks(); @@ -86,19 +71,6 @@ describe('CreateLogicAppVSCodeContents', () => { vi.mocked(fse.ensureDir).mockResolvedValue(undefined); vi.mocked(fse.copyFile).mockResolvedValue(undefined); vi.mocked(fse.pathExists).mockResolvedValue(false); // File doesn't exist - vi.mocked(fse.readFile).mockImplementation(async (filePath: any) => { - const filePathStr = String(filePath); - if (filePathStr.endsWith('ExtensionsJsonFile')) { - return extensionsJsonFileContent; - } - if (filePathStr.endsWith('DevContainerTasksJsonFile')) { - return devContainerTasksJsonFileContent; - } - if (filePathStr.endsWith('TasksJsonFile')) { - return tasksJsonFileContent; - } - return '{}'; - }); vi.mocked(fse.writeJson).mockResolvedValue(undefined); // Mock confirmEditJsonFile to capture what would be written @@ -124,14 +96,10 @@ describe('CreateLogicAppVSCodeContents', () => { it('should create settings.json with correct settings for standard logic app', async () => { await CreateLogicAppVSCodeContentsModule.createLogicAppVsCodeContents(mockContext, logicAppFolderPath); - // Verify confirmEditJsonFile was called for settings.json const settingsJsonPath = path.join(logicAppFolderPath, '.vscode', 'settings.json'); - expect(fsUtils.confirmEditJsonFile).toHaveBeenCalledWith(mockContext, settingsJsonPath, expect.any(Function)); - - // Get the callback function and test what it would write - const settingsCall = vi.mocked(fsUtils.confirmEditJsonFile).mock.calls.find((call) => call[1] === settingsJsonPath); - const settingsCallback = settingsCall[2]; - const settingsData = settingsCallback({}); + const writeCall = vi.mocked(fse.writeJson).mock.calls.find((call) => call[0] === settingsJsonPath); + expect(writeCall).toBeDefined(); + const settingsData = writeCall![1] as Record; // Verify settings content expect(settingsData).toHaveProperty('azureLogicAppsStandard.projectLanguage', 'JavaScript'); @@ -147,9 +115,9 @@ describe('CreateLogicAppVSCodeContents', () => { await CreateLogicAppVSCodeContentsModule.createLogicAppVsCodeContents(mockContextCustomCode, logicAppFolderPath); const settingsJsonPath = path.join(logicAppFolderPath, '.vscode', 'settings.json'); - const settingsCall = vi.mocked(fsUtils.confirmEditJsonFile).mock.calls.find((call) => call[1] === settingsJsonPath); - const settingsCallback = settingsCall[2]; - const settingsData = settingsCallback({}); + const writeCall = vi.mocked(fse.writeJson).mock.calls.find((call) => call[0] === settingsJsonPath); + expect(writeCall).toBeDefined(); + const settingsData = writeCall![1] as Record; // Should have standard settings but NOT deploySubpath expect(settingsData).toHaveProperty('azureFunctions.suppressProject', true); @@ -166,9 +134,9 @@ describe('CreateLogicAppVSCodeContents', () => { await CreateLogicAppVSCodeContentsModule.createLogicAppVsCodeContents(mockContextCustomCodeNet10, logicAppFolderPath); const settingsJsonPath = path.join(logicAppFolderPath, '.vscode', 'settings.json'); - const settingsCall = vi.mocked(fsUtils.confirmEditJsonFile).mock.calls.find((call) => call[1] === settingsJsonPath); - const settingsCallback = settingsCall[2]; - const settingsData = settingsCallback({}); + const writeCall = vi.mocked(fse.writeJson).mock.calls.find((call) => call[0] === settingsJsonPath); + expect(writeCall).toBeDefined(); + const settingsData = writeCall![1] as Record; expect(settingsData).toHaveProperty('azureFunctions.suppressProject', true); expect(settingsData).toHaveProperty('azureLogicAppsStandard.projectLanguage', 'JavaScript'); @@ -182,9 +150,9 @@ describe('CreateLogicAppVSCodeContents', () => { await CreateLogicAppVSCodeContentsModule.createLogicAppVsCodeContents(mockContextCustomCodeNetFx, logicAppFolderPath); const settingsJsonPath = path.join(logicAppFolderPath, '.vscode', 'settings.json'); - const settingsCall = vi.mocked(fsUtils.confirmEditJsonFile).mock.calls.find((call) => call[1] === settingsJsonPath); - const settingsCallback = settingsCall[2]; - const settingsData = settingsCallback({}); + const writeCall = vi.mocked(fse.writeJson).mock.calls.find((call) => call[0] === settingsJsonPath); + expect(writeCall).toBeDefined(); + const settingsData = writeCall![1] as Record; // Should have standard settings but NOT deploySubpath expect(settingsData).toHaveProperty('azureFunctions.suppressProject', true); @@ -201,9 +169,9 @@ describe('CreateLogicAppVSCodeContents', () => { await CreateLogicAppVSCodeContentsModule.createLogicAppVsCodeContents(mockContextRulesEngine, logicAppFolderPath); const settingsJsonPath = path.join(logicAppFolderPath, '.vscode', 'settings.json'); - const settingsCall = vi.mocked(fsUtils.confirmEditJsonFile).mock.calls.find((call) => call[1] === settingsJsonPath); - const settingsCallback = settingsCall[2]; - const settingsData = settingsCallback({}); + const writeCall = vi.mocked(fse.writeJson).mock.calls.find((call) => call[0] === settingsJsonPath); + expect(writeCall).toBeDefined(); + const settingsData = writeCall![1] as Record; // Should have standard settings but NOT deploySubpath expect(settingsData).toHaveProperty('azureFunctions.suppressProject', true); @@ -220,12 +188,9 @@ describe('CreateLogicAppVSCodeContents', () => { await CreateLogicAppVSCodeContentsModule.createLogicAppVsCodeContents(mockContext, logicAppFolderPath); const launchJsonPath = path.join(logicAppFolderPath, '.vscode', 'launch.json'); - expect(fsUtils.confirmEditJsonFile).toHaveBeenCalledWith(mockContext, launchJsonPath, expect.any(Function)); - - // Get the callback and test the configuration - const launchCall = vi.mocked(fsUtils.confirmEditJsonFile).mock.calls.find((call) => call[1] === launchJsonPath); - const launchCallback = launchCall[2]; - const launchData = launchCallback({ configurations: [] }); + const writeCall = vi.mocked(fse.writeJson).mock.calls.find((call) => call[0] === launchJsonPath); + expect(writeCall).toBeDefined(); + const launchData = writeCall![1] as { version: string; configurations: any[] }; // Verify launch.json structure expect(launchData).toHaveProperty('version', '0.2.0'); @@ -244,9 +209,9 @@ describe('CreateLogicAppVSCodeContents', () => { await CreateLogicAppVSCodeContentsModule.createLogicAppVsCodeContents(mockContextCustomCode, logicAppFolderPath); const launchJsonPath = path.join(logicAppFolderPath, '.vscode', 'launch.json'); - const launchCall = vi.mocked(fsUtils.confirmEditJsonFile).mock.calls.find((call) => call[1] === launchJsonPath); - const launchCallback = launchCall[2]; - const launchData = launchCallback({ configurations: [] }); + const writeCall = vi.mocked(fse.writeJson).mock.calls.find((call) => call[0] === launchJsonPath); + expect(writeCall).toBeDefined(); + const launchData = writeCall![1] as { version: string; configurations: any[] }; const config = launchData.configurations[0]; expect(config).toMatchObject({ @@ -263,9 +228,9 @@ describe('CreateLogicAppVSCodeContents', () => { await CreateLogicAppVSCodeContentsModule.createLogicAppVsCodeContents(mockContextCustomCodeNet10, logicAppFolderPath); const launchJsonPath = path.join(logicAppFolderPath, '.vscode', 'launch.json'); - const launchCall = vi.mocked(fsUtils.confirmEditJsonFile).mock.calls.find((call) => call[1] === launchJsonPath); - const launchCallback = launchCall[2]; - const launchData = launchCallback({ configurations: [] }); + const writeCall = vi.mocked(fse.writeJson).mock.calls.find((call) => call[0] === launchJsonPath); + expect(writeCall).toBeDefined(); + const launchData = writeCall![1] as { version: string; configurations: any[] }; const config = launchData.configurations[0]; expect(config).toMatchObject({ @@ -282,9 +247,9 @@ describe('CreateLogicAppVSCodeContents', () => { await CreateLogicAppVSCodeContentsModule.createLogicAppVsCodeContents(mockContextRulesEngine, logicAppFolderPath); const launchJsonPath = path.join(logicAppFolderPath, '.vscode', 'launch.json'); - const launchCall = vi.mocked(fsUtils.confirmEditJsonFile).mock.calls.find((call) => call[1] === launchJsonPath); - const launchCallback = launchCall[2]; - const launchData = launchCallback({ configurations: [] }); + const writeCall = vi.mocked(fse.writeJson).mock.calls.find((call) => call[0] === launchJsonPath); + expect(writeCall).toBeDefined(); + const launchData = writeCall![1] as { version: string; configurations: any[] }; const config = launchData.configurations[0]; expect(config).toMatchObject({ @@ -301,9 +266,9 @@ describe('CreateLogicAppVSCodeContents', () => { await CreateLogicAppVSCodeContentsModule.createLogicAppVsCodeContents(mockContextCustomCodeNetFx, logicAppFolderPath); const launchJsonPath = path.join(logicAppFolderPath, '.vscode', 'launch.json'); - const launchCall = vi.mocked(fsUtils.confirmEditJsonFile).mock.calls.find((call) => call[1] === launchJsonPath); - const launchCallback = launchCall[2]; - const launchData = launchCallback({ configurations: [] }); + const writeCall = vi.mocked(fse.writeJson).mock.calls.find((call) => call[0] === launchJsonPath); + expect(writeCall).toBeDefined(); + const launchData = writeCall![1] as { version: string; configurations: any[] }; const config = launchData.configurations[0]; expect(config).toMatchObject({ @@ -320,9 +285,9 @@ describe('CreateLogicAppVSCodeContents', () => { await CreateLogicAppVSCodeContentsModule.createLogicAppVsCodeContents(mockContextCodeful, logicAppFolderPath); const settingsJsonPath = path.join(logicAppFolderPath, '.vscode', 'settings.json'); - const settingsCall = vi.mocked(fsUtils.confirmEditJsonFile).mock.calls.find((call) => call[1] === settingsJsonPath); - const settingsCallback = settingsCall[2]; - const settingsData = settingsCallback({}); + const writeCall = vi.mocked(fse.writeJson).mock.calls.find((call) => call[0] === settingsJsonPath); + expect(writeCall).toBeDefined(); + const settingsData = writeCall![1] as Record; expect(settingsData).toHaveProperty('azureLogicAppsStandard.projectLanguage', 'C#'); expect(settingsData).toHaveProperty('azureLogicAppsStandard.projectRuntime', '~4'); @@ -340,9 +305,9 @@ describe('CreateLogicAppVSCodeContents', () => { await CreateLogicAppVSCodeContentsModule.createLogicAppVsCodeContents(mockContextCodeful, logicAppFolderPath); const launchJsonPath = path.join(logicAppFolderPath, '.vscode', 'launch.json'); - const launchCall = vi.mocked(fsUtils.confirmEditJsonFile).mock.calls.find((call) => call[1] === launchJsonPath); - const launchCallback = launchCall[2]; - const launchData = launchCallback({ configurations: [] }); + const writeCall = vi.mocked(fse.writeJson).mock.calls.find((call) => call[0] === launchJsonPath); + expect(writeCall).toBeDefined(); + const launchData = writeCall![1] as { version: string; configurations: any[] }; const config = launchData.configurations[0]; expect(config).toMatchObject({ @@ -355,11 +320,14 @@ describe('CreateLogicAppVSCodeContents', () => { expect(config).not.toHaveProperty('customCodeRuntime'); }); - it('should copy extensions.json from template', async () => { + it('should write extensions.json with Logic Apps extension recommendation', async () => { await CreateLogicAppVSCodeContentsModule.createLogicAppVsCodeContents(mockContext, logicAppFolderPath); const extensionsJsonPath = path.join(logicAppFolderPath, '.vscode', 'extensions.json'); - expect(fse.writeJson).toHaveBeenCalledWith(extensionsJsonPath, JSON.parse(extensionsJsonFileContent), { spaces: 2 }); + const writeCall = vi.mocked(fse.writeJson).mock.calls.find((call) => call[0] === extensionsJsonPath); + expect(writeCall).toBeDefined(); + expect(writeCall[1]).toHaveProperty('recommendations'); + expect((writeCall[1] as any).recommendations).toContain('ms-azuretools.vscode-azurelogicapps'); }); it('should use an extensions.json template that does not recommend Dev Containers', async () => { @@ -372,20 +340,27 @@ describe('CreateLogicAppVSCodeContents', () => { expect(extensionsData.recommendations).not.toContain('ms-vscode-remote.remote-containers'); }); - it('should copy tasks.json from template', async () => { + it('should write tasks.json via generator', async () => { await CreateLogicAppVSCodeContentsModule.createLogicAppVsCodeContents(mockContext, logicAppFolderPath); const tasksJsonPath = path.join(logicAppFolderPath, '.vscode', 'tasks.json'); - expect(fse.copyFile).toHaveBeenCalledWith(expect.stringContaining('TasksJsonFile'), tasksJsonPath); + const writeCall = vi.mocked(fse.writeJson).mock.calls.find((call) => call[0] === tasksJsonPath); + expect(writeCall).toBeDefined(); + expect(writeCall[1]).toHaveProperty('version', '2.0.0'); + expect(writeCall[1]).toHaveProperty('tasks'); }); - it('should copy DevContainerTasksJsonFile when isDevContainerProject is true', async () => { + it('should write tasks.json without platform env when isDevContainerProject is true', async () => { const devContainerContext = { ...mockContext, isDevContainerProject: true }; await CreateLogicAppVSCodeContentsModule.createLogicAppVsCodeContents(devContainerContext, logicAppFolderPath); const tasksJsonPath = path.join(logicAppFolderPath, '.vscode', 'tasks.json'); - expect(fse.copyFile).toHaveBeenCalledWith(expect.stringContaining('DevContainerTasksJsonFile'), tasksJsonPath); + const writeCall = vi.mocked(fse.writeJson).mock.calls.find((call) => call[0] === tasksJsonPath); + expect(writeCall).toBeDefined(); + const funcTask = (writeCall[1] as any).tasks.find((t: any) => t.label === 'func: host start'); + expect(funcTask).toBeDefined(); + expect(funcTask.windows).toBeUndefined(); }); }); @@ -417,66 +392,4 @@ describe('CreateLogicAppVSCodeContents', () => { expect(fse.copyFile).not.toHaveBeenCalled(); }); }); - - describe('getDebugConfiguration', () => { - it('should return attach configuration for standard logic app', () => { - const config = CreateLogicAppVSCodeContentsModule.getDebugConfiguration('TestLogicApp'); - - expect(config).toMatchObject({ - name: expect.stringContaining('TestLogicApp'), - type: 'coreclr', - request: 'attach', - processId: expect.any(String), - }); - }); - - it('should return logicapp configuration with coreclr for Net8 custom code', () => { - const config = CreateLogicAppVSCodeContentsModule.getDebugConfiguration('TestLogicApp', TargetFramework.Net8); - - expect(config).toMatchObject({ - type: 'logicapp', - request: 'launch', - funcRuntime: 'coreclr', - customCodeRuntime: 'coreclr', - isCodeless: true, - }); - }); - - it('should return logicapp configuration with coreclr for Net10 custom code', () => { - const config = CreateLogicAppVSCodeContentsModule.getDebugConfiguration('TestLogicApp', TargetFramework.Net10); - - expect(config).toMatchObject({ - type: 'logicapp', - request: 'launch', - funcRuntime: 'coreclr', - customCodeRuntime: 'coreclr', - isCodeless: true, - }); - }); - - it('should return logicapp configuration with clr for NetFx custom code', () => { - const config = CreateLogicAppVSCodeContentsModule.getDebugConfiguration('TestLogicApp', TargetFramework.NetFx); - - expect(config).toMatchObject({ - type: 'logicapp', - request: 'launch', - funcRuntime: 'coreclr', - customCodeRuntime: 'clr', - isCodeless: true, - }); - }); - - it('should return logicapp configuration with isCodeless false for codeful project', () => { - const config = CreateLogicAppVSCodeContentsModule.getDebugConfiguration('TestLogicApp', undefined, true); - - expect(config).toMatchObject({ - name: expect.stringContaining('Run/Debug logic app TestLogicApp'), - type: 'logicapp', - request: 'launch', - funcRuntime: 'coreclr', - isCodeless: false, - }); - expect(config).not.toHaveProperty('customCodeRuntime'); - }); - }); }); diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/__test__/CreateLogicAppWorkspaceIntegration.test.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/__test__/CreateLogicAppWorkspaceIntegration.test.ts index 2be494960cc..b13f5feb38b 100644 --- a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/__test__/CreateLogicAppWorkspaceIntegration.test.ts +++ b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/__test__/CreateLogicAppWorkspaceIntegration.test.ts @@ -13,6 +13,12 @@ import { devContainerFolderName, devContainerFileName } from '../../../../../con // Unmock fs-extra to use real file operations for integration tests vi.unmock('fs-extra'); +// Mock binariesExistSync to avoid VS Code workspace API calls in test +vi.mock('../../../../utils/binaries', () => ({ + binariesExistSync: vi.fn().mockReturnValue(true), + binariesExist: vi.fn().mockReturnValue(true), +})); + // Import fs-extra after unmocking import * as fse from 'fs-extra'; import * as CreateLogicAppWorkspaceModule from '../CreateLogicAppWorkspace'; diff --git a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/AppNamespaceStep.ts b/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/AppNamespaceStep.ts deleted file mode 100644 index 6cc04f2d66d..00000000000 --- a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/AppNamespaceStep.ts +++ /dev/null @@ -1,42 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import { namespaceValidation } from '../../../../constants'; -import { ext } from '../../../../extensionVariables'; -import { localize } from '../../../../localize'; -import { AzureWizardPromptStep } from '@microsoft/vscode-azext-utils'; -import type { IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps'; - -export class AppNamespaceStep extends AzureWizardPromptStep { - public hideStepCount = true; - - public shouldPrompt(): boolean { - return true; - } - - public async prompt(context: IProjectWizardContext): Promise { - context.functionAppNamespace = await context.ui.showInputBox({ - placeHolder: localize('setNamespace', 'Namespace'), - prompt: localize('methodNamePrompt', 'Provide a namespace for functions project'), - validateInput: async (input: string): Promise => await this.validateNamespace(input), - }); - - ext.outputChannel.appendLog( - localize('functionAppNamespaceSet', `Function App project namespace set to ${context.functionAppNamespace}`) - ); - } - - private async validateNamespace(namespace: string | undefined): Promise { - if (!namespace) { - return localize('emptyNamespaceError', `Can't have an empty namespace.`); - } - - if (!namespaceValidation.test(namespace)) { - return localize( - 'namespaceInvalidMessage', - 'The namespace must start with a letter or underscore, contain only letters, digits, underscores, and periods, and must not end with a period.' - ); - } - } -} diff --git a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/__test__/AppNamespaceStep.test.ts b/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/__test__/AppNamespaceStep.test.ts deleted file mode 100644 index ecd9548980e..00000000000 --- a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/__test__/AppNamespaceStep.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { AppNamespaceStep } from '../AppNamespaceStep'; -import { ProjectType } from '@microsoft/vscode-extension-logic-apps'; -import { ext } from '../../../../../extensionVariables'; - -describe('AppNamespaceStep', () => { - const validFunctionAppNamespace = 'Valid_Namespace1'; - const invalidFunctionAppNamespace = 'Invalid-Namespace'; - - let appNamespaceStep: AppNamespaceStep; - let testContext: any; - let appendLogSpy: any; - - beforeEach(() => { - appNamespaceStep = new AppNamespaceStep(); - testContext = { - projectType: ProjectType.customCode, - ui: { - showInputBox: vi.fn((options: any) => { - return options.validateInput(options.testInput).then((validationResult: string | undefined) => { - if (validationResult) { - return Promise.reject(new Error(validationResult)); - } - return Promise.resolve(options.testInput); - }); - }), - }, - }; - appendLogSpy = vi.spyOn(ext.outputChannel, 'appendLog').mockImplementation(() => {}); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe('prompt', () => { - it('sets context.functionAppNamespace and logs output for valid input', async () => { - testContext.ui.showInputBox = vi.fn((options: any) => { - options.testInput = validFunctionAppNamespace; - return options.validateInput(options.testInput).then((result: string | undefined) => { - if (result) { - return Promise.reject(new Error(result)); - } - return Promise.resolve(options.testInput); - }); - }); - - await appNamespaceStep.prompt(testContext); - expect(testContext.functionAppNamespace).toBe(validFunctionAppNamespace); - expect(appendLogSpy).toHaveBeenCalledWith(`Function App project namespace set to ${validFunctionAppNamespace}`); - }); - - it('rejects when input is invalid (empty)', async () => { - const emptyNamespace = ''; - testContext.ui.showInputBox = vi.fn((options: any) => { - options.testInput = emptyNamespace; - return options.validateInput(options.testInput).then((result: string | undefined) => { - if (result) { - return Promise.reject(new Error(result)); - } - return Promise.resolve(emptyNamespace); - }); - }); - - await expect(appNamespaceStep.prompt(testContext)).rejects.toThrowError(`Can't have an empty namespace.`); - }); - }); - - describe('validateNamespace', () => { - const callValidateNamespace = (name: string | undefined) => (appNamespaceStep as any).validateNamespace(name); - - it('returns error for empty namespace', async () => { - const result = await callValidateNamespace(''); - expect(result).toBe(`Can't have an empty namespace.`); - }); - - it('returns error when namespace does not pass regex validation', async () => { - const result = await callValidateNamespace(invalidFunctionAppNamespace); - expect(result).toBe( - 'The namespace must start with a letter or underscore, contain only letters, digits, underscores, and periods, and must not end with a period.' - ); - }); - - it('returns undefined for valid namespace', async () => { - const result = await callValidateNamespace(validFunctionAppNamespace); - expect(result).toBeUndefined(); - }); - }); -}); diff --git a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/__test__/FunctionAppNameStep.test.ts b/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/__test__/FunctionAppNameStep.test.ts deleted file mode 100644 index c839378d78e..00000000000 --- a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/__test__/FunctionAppNameStep.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import * as fse from 'fs-extra'; -import * as vscode from 'vscode'; -import * as path from 'path'; -import { FunctionAppNameStep } from '../functionAppNameStep'; -import { ext } from '../../../../../extensionVariables'; -import { localize } from '../../../../../localize'; -import { IProjectWizardContext, ProjectType } from '@microsoft/vscode-extension-logic-apps'; - -describe('FunctionAppNameStep', () => { - const existingFunctionAppName = 'Func1'; - const validFunctionAppName = 'Valid_Func_'; - const invalidFunctionAppName = 'Invalid-Func'; - - const testLogicAppName = 'LogicApp1'; - const testWorkspaceName = 'TestWorkspace'; - const testWorkspaceFile = path.join('path', 'to', `${testWorkspaceName}.code-workspace`); - const testWorkspace = { - folders: [{ name: existingFunctionAppName }], - }; - let functionAppNameStep: FunctionAppNameStep; - let testContext: any; - let existsSyncSpy: any; - let appendLogSpy: any; - let readFileSpy: any; - - beforeEach(() => { - functionAppNameStep = new FunctionAppNameStep(); - testContext = { - projectType: ProjectType.customCode, - workspaceCustomFilePath: testWorkspaceFile, - logicAppName: testLogicAppName, - ui: { - showInputBox: vi.fn((options: any) => { - return options.validateInput(options.testInput).then((validationResult: string | undefined) => { - if (validationResult) { - return Promise.reject(new Error(validationResult)); - } - return Promise.resolve(options.testInput); - }); - }), - }, - }; - - appendLogSpy = vi.spyOn(ext.outputChannel, 'appendLog').mockImplementation(() => {}); - existsSyncSpy = vi.spyOn(fse, 'existsSync').mockReturnValue(true); - readFileSpy = vi.spyOn(vscode.workspace.fs, 'readFile').mockResolvedValue(Buffer.from(JSON.stringify(testWorkspace))); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe('prompt', () => { - it('sets context.functionAppName and logs output for valid input', async () => { - testContext.ui.showInputBox = vi.fn((options: any) => { - options.testInput = validFunctionAppName; - return options.validateInput(options.testInput).then((result: string | undefined) => { - if (result) { - return Promise.reject(new Error(result)); - } - return Promise.resolve(options.testInput); - }); - }); - await functionAppNameStep.prompt(testContext); - expect(testContext.functionAppName).toBe(validFunctionAppName); - expect(appendLogSpy).toHaveBeenCalledWith(localize('functionAppNameSet', `Function App project name set to ${validFunctionAppName}`)); - }); - - it('rejects when input is invalid (empty)', async () => { - const emptyName = ''; - testContext.ui.showInputBox = vi.fn((options: any) => { - options.testInput = emptyName; - return options.validateInput(options.testInput).then((result: string | undefined) => { - if (result) { - return Promise.reject(new Error(result)); - } - return Promise.resolve(emptyName); - }); - }); - await expect(functionAppNameStep.prompt(testContext)).rejects.toThrow( - localize('emptyFunctionNameError', "Can't have an empty function name.") - ); - }); - }); - - describe('validateFunctionName', () => { - const callValidateFunctionName = (name: string | undefined, context: IProjectWizardContext) => - (functionAppNameStep as any).validateFunctionName(name, context); - - it('returns error for empty function name', async () => { - const emptyName = ''; - const result = await callValidateFunctionName(emptyName, testContext); - expect(result).toBe(localize('emptyFunctionNameError', "Can't have an empty function name.")); - }); - - it('returns error when function name is same as logic app name', async () => { - const result = await callValidateFunctionName(testLogicAppName, testContext); - expect(result).toBe( - localize('functionNameSameAsProjectNameError', `Can't use the same name for the function and the logic app project.`) - ); - }); - - it('returns error when function name already exists in workspace', async () => { - const result = await callValidateFunctionName(existingFunctionAppName, testContext); - expect(result).toBe(localize('functionNameExistsInWorkspaceError', 'A function with this name already exists in the workspace.')); - }); - - it('returns error when function name does not pass regex validation', async () => { - const result = await callValidateFunctionName(invalidFunctionAppName, testContext); - expect(result).toBe( - localize( - 'functionNameInvalidMessage', - 'The function name must start with a letter and can only contain letters, digits, or underscores ("_").' - ) - ); - }); - - it('returns undefined for valid function name', async () => { - const result = await callValidateFunctionName(validFunctionAppName, testContext); - expect(result).toBeUndefined(); - }); - }); -}); diff --git a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/__test__/FunctionAppNamespaceStep.test.ts b/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/__test__/FunctionAppNamespaceStep.test.ts deleted file mode 100644 index a3796e67fbc..00000000000 --- a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/__test__/FunctionAppNamespaceStep.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { FunctionAppNamespaceStep } from '../functionAppNamespaceStep'; -import { ProjectType } from '@microsoft/vscode-extension-logic-apps'; -import { ext } from '../../../../../extensionVariables'; - -describe('FunctionAppNamespaceStep', () => { - const validFunctionAppNamespace = 'Valid_Namespace1'; - const invalidFunctionAppNamespace = 'Invalid-Namespace'; - - let functionAppNamespaceStep: FunctionAppNamespaceStep; - let testContext: any; - let appendLogSpy: any; - - beforeEach(() => { - functionAppNamespaceStep = new FunctionAppNamespaceStep(); - testContext = { - projectType: ProjectType.customCode, - ui: { - showInputBox: vi.fn((options: any) => { - return options.validateInput(options.testInput).then((validationResult: string | undefined) => { - if (validationResult) { - return Promise.reject(new Error(validationResult)); - } - return Promise.resolve(options.testInput); - }); - }), - }, - }; - appendLogSpy = vi.spyOn(ext.outputChannel, 'appendLog').mockImplementation(() => {}); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe('prompt', () => { - it('sets context.functionAppNamespace and logs output for valid input', async () => { - testContext.ui.showInputBox = vi.fn((options: any) => { - options.testInput = validFunctionAppNamespace; - return options.validateInput(options.testInput).then((result: string | undefined) => { - if (result) { - return Promise.reject(new Error(result)); - } - return Promise.resolve(options.testInput); - }); - }); - - await functionAppNamespaceStep.prompt(testContext); - expect(testContext.functionAppNamespace).toBe(validFunctionAppNamespace); - expect(appendLogSpy).toHaveBeenCalledWith(`Function App project namespace set to ${validFunctionAppNamespace}`); - }); - - it('rejects when input is invalid (empty)', async () => { - const emptyNamespace = ''; - testContext.ui.showInputBox = vi.fn((options: any) => { - options.testInput = emptyNamespace; - return options.validateInput(options.testInput).then((result: string | undefined) => { - if (result) { - return Promise.reject(new Error(result)); - } - return Promise.resolve(emptyNamespace); - }); - }); - - await expect(functionAppNamespaceStep.prompt(testContext)).rejects.toThrowError(`Can't have an empty namespace.`); - }); - }); - - describe('validateNamespace', () => { - const callValidateNamespace = (name: string | undefined) => (functionAppNamespaceStep as any).validateNamespace(name); - - it('returns error for empty namespace', async () => { - const result = await callValidateNamespace(''); - expect(result).toBe(`Can't have an empty namespace.`); - }); - - it('returns error when namespace does not pass regex validation', async () => { - const result = await callValidateNamespace(invalidFunctionAppNamespace); - expect(result).toBe( - 'The namespace must start with a letter or underscore, contain only letters, digits, underscores, and periods, and must not end with a period.' - ); - }); - - it('returns undefined for valid namespace', async () => { - const result = await callValidateNamespace(validFunctionAppNamespace); - expect(result).toBeUndefined(); - }); - - it('returns undefined for dotted namespace like MyCompany.Functions', async () => { - const result = await callValidateNamespace('MyCompany.Functions'); - expect(result).toBeUndefined(); - }); - - it('returns undefined for multi-segment dotted namespace', async () => { - const result = await callValidateNamespace('A.B.C'); - expect(result).toBeUndefined(); - }); - - it('returns error for namespace ending with a dot', async () => { - const result = await callValidateNamespace('MyNamespace.'); - expect(result).toBeDefined(); - }); - - it('returns error for namespace starting with a dot', async () => { - const result = await callValidateNamespace('.MyNamespace'); - expect(result).toBeDefined(); - }); - - it('returns error for namespace with consecutive dots', async () => { - const result = await callValidateNamespace('A..B'); - expect(result).toBeDefined(); - }); - }); -}); diff --git a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/__test__/functionAppFilesStep.test.ts b/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/__test__/functionAppFilesStep.test.ts deleted file mode 100644 index 39bad5dbff0..00000000000 --- a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/__test__/functionAppFilesStep.test.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { TargetFramework, ProjectType } from '@microsoft/vscode-extension-logic-apps'; - -vi.mock('fs-extra', () => ({ - writeFile: vi.fn(() => Promise.resolve()), - ensureDir: vi.fn(() => Promise.resolve()), - readFile: vi.fn(() => Promise.resolve('')), - pathExists: vi.fn(() => Promise.resolve(false)), - existsSync: vi.fn(() => false), - readdir: vi.fn(), - stat: vi.fn(), - writeJson: vi.fn(() => Promise.resolve()), - copyFile: vi.fn(() => Promise.resolve()), - readJson: vi.fn(() => Promise.resolve({})), -})); -vi.mock('vscode'); -vi.mock('../../../../../constants', async (importOriginal) => { - const actual = (await importOriginal()) as Record; - return { - ...actual, - }; -}); -vi.mock('../../../../../extensionVariables', () => ({ - ext: { outputChannel: { appendLog: vi.fn() } }, -})); -vi.mock('../../../../../localize', () => ({ - localize: (_key: string, msg: string) => msg, -})); -vi.mock('../../../../utils/vsCodeConfig/launch', () => ({ - getDebugConfigs: vi.fn().mockReturnValue([]), - updateDebugConfigs: vi.fn(), -})); -vi.mock('../../../../utils/workspace', () => ({ - getContainingWorkspace: vi.fn(), - isMultiRootWorkspace: vi.fn().mockReturnValue(false), -})); -vi.mock('../../../../utils/funcCoreTools/funcVersion', () => ({ - tryGetLocalFuncVersion: vi.fn().mockResolvedValue('~4'), -})); -vi.mock('../../../../utils/debug', () => ({ - getCustomCodeRuntime: vi.fn((tf: string) => (tf === 'net472' ? 'clr' : 'coreclr')), - getDebugConfiguration: vi.fn().mockReturnValue({}), - usesPublishFolderProperty: vi.fn((pt: string, tf: string) => pt === 'customCode' && tf !== 'net472'), -})); - -import { FunctionAppFilesStep } from '../functionAppFilesStep'; -import { csTemplateFileNames, csprojTemplateFileNames } from '../../../../utils/functionProjectFiles'; -import * as fs from 'fs-extra'; -import type { IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps'; - -describe('FunctionAppFilesStep', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('template name mappings', () => { - it('should map Net10 to FunctionsFileNet10 in csTemplateFileNames', () => { - expect(csTemplateFileNames[TargetFramework.Net10]).toBe('FunctionsFileNet10'); - }); - - it('should preserve Net8 mapping in csTemplateFileNames', () => { - expect(csTemplateFileNames[TargetFramework.Net8]).toBe('FunctionsFileNet8'); - }); - - it('should preserve NetFx mapping in csTemplateFileNames', () => { - expect(csTemplateFileNames[TargetFramework.NetFx]).toBe('FunctionsFileNetFx'); - }); - - it('should preserve rulesEngine mapping in csTemplateFileNames', () => { - expect(csTemplateFileNames[ProjectType.rulesEngine]).toBe('RulesFunctionsFile'); - }); - - it('should map Net10 to FunctionsProjNet10 in csprojTemplateFileNames', () => { - expect(csprojTemplateFileNames[TargetFramework.Net10]).toBe('FunctionsProjNet10'); - }); - - it('should preserve Net8 mapping in csprojTemplateFileNames', () => { - expect(csprojTemplateFileNames[TargetFramework.Net8]).toBe('FunctionsProjNet8'); - }); - - it('should preserve NetFx mapping in csprojTemplateFileNames', () => { - expect(csprojTemplateFileNames[TargetFramework.NetFx]).toBe('FunctionsProjNetFx'); - }); - - it('should preserve rulesEngine mapping in csprojTemplateFileNames', () => { - expect(csprojTemplateFileNames[ProjectType.rulesEngine]).toBe('RulesFunctionsProj'); - }); - }); - - describe('shouldPrompt', () => { - it('should always return true', () => { - const step = new FunctionAppFilesStep(); - expect(step.shouldPrompt()).toBe(true); - }); - }); - - describe('Program.cs generation via prompt', () => { - function createMockContext(overrides: Partial = {}): IProjectWizardContext { - return { - functionAppName: 'TestFunction', - functionAppNamespace: 'TestNamespace', - targetFramework: TargetFramework.Net10, - logicAppName: 'TestLogicApp', - version: '~4', - workspacePath: '/mock/workspace', - projectType: ProjectType.customCode, - shouldCreateLogicAppProject: true, - ...overrides, - } as IProjectWizardContext; - } - - beforeEach(() => { - vi.mocked(fs.ensureDir).mockResolvedValue(undefined); - vi.mocked(fs.readFile).mockResolvedValue('template with <%= namespace %> placeholder'); - vi.mocked(fs.writeFile).mockResolvedValue(undefined); - vi.mocked(fs.writeJson).mockResolvedValue(undefined); - vi.mocked(fs.copyFile).mockResolvedValue(undefined); - vi.mocked(fs.pathExists).mockResolvedValue(false); - }); - - it('should create Program.cs for Net10 custom code project', async () => { - const step = new FunctionAppFilesStep(); - const context = createMockContext({ - targetFramework: TargetFramework.Net10, - projectType: ProjectType.customCode, - }); - - await step.prompt(context); - - const writeFileCalls = vi.mocked(fs.writeFile).mock.calls; - const programCsCall = writeFileCalls.find((call) => String(call[0]).endsWith('Program.cs')); - expect(programCsCall).toBeDefined(); - expect(String(programCsCall![1])).not.toContain('<%= namespace %>'); - }); - - it('should replace namespace placeholder in Program.cs', async () => { - vi.mocked(fs.readFile).mockResolvedValue('namespace <%= namespace %>\n{\n class Program {}\n}'); - const step = new FunctionAppFilesStep(); - const context = createMockContext({ - targetFramework: TargetFramework.Net10, - projectType: ProjectType.customCode, - functionAppNamespace: 'MyCompany.Functions', - }); - - await step.prompt(context); - - const writeFileCalls = vi.mocked(fs.writeFile).mock.calls; - const programCsCall = writeFileCalls.find((call) => String(call[0]).endsWith('Program.cs')); - expect(programCsCall).toBeDefined(); - expect(String(programCsCall![1])).toContain('namespace MyCompany.Functions'); - expect(String(programCsCall![1])).not.toContain('<%= namespace %>'); - }); - - it('should not create Program.cs for Net8 custom code project', async () => { - const step = new FunctionAppFilesStep(); - const context = createMockContext({ - targetFramework: TargetFramework.Net8, - projectType: ProjectType.customCode, - }); - - await step.prompt(context); - - const writeFileCalls = vi.mocked(fs.writeFile).mock.calls; - const programCsCall = writeFileCalls.find((call) => String(call[0]).endsWith('Program.cs')); - expect(programCsCall).toBeUndefined(); - }); - - it('should not create Program.cs for NetFx custom code project', async () => { - const step = new FunctionAppFilesStep(); - const context = createMockContext({ - targetFramework: TargetFramework.NetFx, - projectType: ProjectType.customCode, - }); - - await step.prompt(context); - - const writeFileCalls = vi.mocked(fs.writeFile).mock.calls; - const programCsCall = writeFileCalls.find((call) => String(call[0]).endsWith('Program.cs')); - expect(programCsCall).toBeUndefined(); - }); - - it('should not create Program.cs for rulesEngine project even with Net10', async () => { - const step = new FunctionAppFilesStep(); - const context = createMockContext({ - targetFramework: TargetFramework.Net10, - projectType: ProjectType.rulesEngine, - }); - - await step.prompt(context); - - const writeFileCalls = vi.mocked(fs.writeFile).mock.calls; - const programCsCall = writeFileCalls.find((call) => String(call[0]).endsWith('Program.cs')); - expect(programCsCall).toBeUndefined(); - }); - }); -}); diff --git a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/__test__/targetFrameworkStep.test.ts b/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/__test__/targetFrameworkStep.test.ts deleted file mode 100644 index b79a1f184d6..00000000000 --- a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/__test__/targetFrameworkStep.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { describe, it, expect, vi } from 'vitest'; -import { ProjectType, TargetFramework } from '@microsoft/vscode-extension-logic-apps'; - -vi.mock('../../../../../localize', () => ({ - localize: (_key: string, msg: string) => msg, -})); - -import { TargetFrameworkStep } from '../targetFrameworkStep'; - -describe('TargetFrameworkStep', () => { - describe('shouldPrompt', () => { - it('should return true for customCode projects', () => { - const step = new TargetFrameworkStep(); - const context = { projectType: ProjectType.customCode } as any; - expect(step.shouldPrompt(context)).toBe(true); - }); - - it('should return false for logicApp projects', () => { - const step = new TargetFrameworkStep(); - const context = { projectType: ProjectType.logicApp } as any; - expect(step.shouldPrompt(context)).toBe(false); - }); - - it('should return false for rulesEngine projects', () => { - const step = new TargetFrameworkStep(); - const context = { projectType: ProjectType.rulesEngine } as any; - expect(step.shouldPrompt(context)).toBe(false); - }); - }); - - describe('prompt', () => { - it('should offer .NET 8 and .NET 10 picks on non-Windows platforms', async () => { - const originalPlatform = process.platform; - Object.defineProperty(process, 'platform', { value: 'darwin' }); - - const step = new TargetFrameworkStep(); - let capturedPicks: any[] = []; - const context = { - projectType: ProjectType.customCode, - ui: { - showQuickPick: vi.fn((picks: any[]) => { - capturedPicks = picks; - return Promise.resolve(picks[0]); - }), - }, - } as any; - - await step.prompt(context); - - expect(capturedPicks).toHaveLength(2); - expect(capturedPicks[0].data).toBe(TargetFramework.Net8); - expect(capturedPicks[1].data).toBe(TargetFramework.Net10); - - Object.defineProperty(process, 'platform', { value: originalPlatform }); - }); - - it('should include .NET Framework on Windows', async () => { - const originalPlatform = process.platform; - Object.defineProperty(process, 'platform', { value: 'win32' }); - - const step = new TargetFrameworkStep(); - let capturedPicks: any[] = []; - const context = { - projectType: ProjectType.customCode, - ui: { - showQuickPick: vi.fn((picks: any[]) => { - capturedPicks = picks; - return Promise.resolve(picks[0]); - }), - }, - } as any; - - await step.prompt(context); - - expect(capturedPicks).toHaveLength(3); - expect(capturedPicks[0].data).toBe(TargetFramework.NetFx); - expect(capturedPicks[1].data).toBe(TargetFramework.Net8); - expect(capturedPicks[2].data).toBe(TargetFramework.Net10); - - Object.defineProperty(process, 'platform', { value: originalPlatform }); - }); - - it('should set context.targetFramework to the selected value', async () => { - const originalPlatform = process.platform; - Object.defineProperty(process, 'platform', { value: 'darwin' }); - - const step = new TargetFrameworkStep(); - const context = { - projectType: ProjectType.customCode, - ui: { - showQuickPick: vi.fn((picks: any[]) => { - // Simulate selecting .NET 10 - const net10Pick = picks.find((p: any) => p.data === TargetFramework.Net10); - return Promise.resolve(net10Pick); - }), - }, - } as any; - - await step.prompt(context); - expect(context.targetFramework).toBe(TargetFramework.Net10); - - Object.defineProperty(process, 'platform', { value: originalPlatform }); - }); - }); -}); diff --git a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/functionAppFilesStep.ts b/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/functionAppFilesStep.ts deleted file mode 100644 index 10c613498ce..00000000000 --- a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/functionAppFilesStep.ts +++ /dev/null @@ -1,204 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import { - dotnetExtensionId, - functionsExtensionId, - vscodeFolderName, - extensionsFileName, - launchFileName, - settingsFileName, - tasksFileName, - extensionCommand, - assetsFolderName, -} from '../../../../constants'; -import { AzureWizardPromptStep } from '@microsoft/vscode-azext-utils'; -import type { FuncVersion, IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps'; -import { TargetFramework, ProjectType } from '@microsoft/vscode-extension-logic-apps'; -import * as fs from 'fs-extra'; -import * as path from 'path'; -import { getDebugConfigs, updateDebugConfigs } from '../../../utils/vsCodeConfig/launch'; -import { getContainingWorkspace, isMultiRootWorkspace } from '../../../utils/workspace'; -import { tryGetLocalFuncVersion } from '../../../utils/funcCoreTools/funcVersion'; -import { getCustomCodeRuntime, getDebugConfiguration } from '../../../utils/debug'; -import { createCsFile, createProgramFile, createRulesFiles, createCsprojFile } from '../../../utils/functionProjectFiles'; - -/** - * This class represents a prompt step that allows the user to set up an Azure Function project. - */ -export class FunctionAppFilesStep extends AzureWizardPromptStep { - public hideStepCount = true; - - /** - * Determines whether the prompt should be displayed. - * @returns {boolean} True if the prompt should be displayed, false otherwise. - */ - public shouldPrompt(): boolean { - return true; - } - - /** - * Prompts the user to set up an Azure Function project. - * @param context The project wizard context. - */ - public async prompt(context: IProjectWizardContext): Promise { - const { functionAppName, functionAppNamespace: namespace, targetFramework, projectType } = context; - const logicAppName = context.logicAppName || 'LogicApp'; - const funcVersion = context.version ?? (await tryGetLocalFuncVersion()); - const functionFolderPath = path.join(context.workspacePath, context.functionAppName); - const assetsPath = path.join(__dirname, assetsFolderName); - - await fs.ensureDir(functionFolderPath); - await createCsFile(assetsPath, functionFolderPath, functionAppName, namespace, projectType, targetFramework); - await createProgramFile(assetsPath, functionFolderPath, namespace, projectType, targetFramework); - - if (projectType === ProjectType.rulesEngine) { - await createRulesFiles(assetsPath, functionFolderPath); - } - - await createCsprojFile(assetsPath, functionFolderPath, functionAppName, logicAppName, projectType, targetFramework); - - const isNewLogicAppProject = context.shouldCreateLogicAppProject; - await this.createVscodeConfigFiles(functionFolderPath, targetFramework, funcVersion, logicAppName, isNewLogicAppProject); - } - - /** - * Creates the Visual Studio Code configuration files in the .vscode folder of the specified functions app. - * @param functionFolderPath The path to the functions folder. - * @param targetFramework The target framework of the functions app. - * @param funcVersion The version of the functions app. - * @param logicAppName The name of the logic app. - * @param isNewLogicAppProject Indicates if the logic app project is new. - */ - private async createVscodeConfigFiles( - functionFolderPath: string, - targetFramework: TargetFramework, - funcVersion: FuncVersion, - logicAppName: string, - isNewLogicAppProject: boolean - ): Promise { - await fs.ensureDir(functionFolderPath); - const vscodePath: string = path.join(functionFolderPath, vscodeFolderName); - await fs.ensureDir(vscodePath); - - await this.generateExtensionsJson(vscodePath); - - // Update launch config for existing logic app project (new projects will be created with the correct config) - if (!isNewLogicAppProject) { - await this.updateLogicAppLaunchJson(vscodePath, targetFramework, funcVersion, logicAppName); - } - - await this.generateSettingsJson(vscodePath, targetFramework); - - await this.generateTasksJson(vscodePath); - } - - /** - * Generates the extensions.json file in the specified folder. - * @param folderPath The path to the folder where the extensions.json file should be generated. - */ - private async generateExtensionsJson(folderPath: string): Promise { - const filePath = path.join(folderPath, extensionsFileName); - const content = { - recommendations: [functionsExtensionId, dotnetExtensionId], - }; - await fs.writeJson(filePath, content, { spaces: 2 }); - } - - /** - * Updates the launch.json file for the logic app corresponding to this functions app. - * @param folderPath The functions app folder path. - * @param targetFramework The target framework of the functions app. - * @param funcVersion The version of the functions app. - * @param logicAppName The name of the logic app. - */ - private async updateLogicAppLaunchJson( - folderPath: string, - targetFramework: TargetFramework, - funcVersion: FuncVersion, - logicAppName: string - ): Promise { - const logicAppLaunchJsonPath = path.join(folderPath, '..', '..', logicAppName, vscodeFolderName, launchFileName); - const logicAppWorkspaceFolder = getContainingWorkspace(logicAppLaunchJsonPath); - const debugConfigs = getDebugConfigs(logicAppWorkspaceFolder); - const updatedDebugConfigs = debugConfigs.some((debugConfig) => debugConfig.type === 'logicapp') - ? debugConfigs.map((debugConfig) => { - // Update the logic app debug configuration to use the correct runtime for custom code - if (debugConfig.type === 'logicapp') { - return { - ...debugConfig, - customCodeRuntime: getCustomCodeRuntime(targetFramework), - isCodeless: true, - }; - } - return debugConfig; - }) - : [ - getDebugConfiguration(funcVersion, logicAppName, targetFramework), - ...debugConfigs.filter( - (debugConfig) => debugConfig.request !== 'attach' || debugConfig.processId !== `\${command:${extensionCommand.pickProcess}}` - ), - ]; - - if (isMultiRootWorkspace()) { - let launchJsonContent: any; - if (await fs.pathExists(logicAppLaunchJsonPath)) { - launchJsonContent = await fs.readJson(logicAppLaunchJsonPath); - launchJsonContent['configurations'] = updatedDebugConfigs; - } else { - launchJsonContent = { - version: '0.2.0', - configurations: updatedDebugConfigs, - }; - } - await fs.writeJson(logicAppLaunchJsonPath, launchJsonContent, { spaces: 2 }); - } else { - updateDebugConfigs(logicAppWorkspaceFolder, updatedDebugConfigs); - } - } - - /** - * Generates the settings.json file in the specified folder. - * @param folderPath The path to the folder where the settings.json file should be generated. - * @param targetFramework The target framework of the functions app. - */ - private async generateSettingsJson(folderPath: string, targetFramework: TargetFramework): Promise { - const filePath = path.join(folderPath, settingsFileName); - const content = { - 'azureFunctions.deploySubpath': `bin/Release/${targetFramework ?? TargetFramework.NetFx}/publish`, - 'azureFunctions.projectLanguage': 'C#', - 'azureFunctions.projectRuntime': '~4', - 'debug.internalConsoleOptions': 'neverOpen', - 'azureFunctions.preDeployTask': 'publish (functions)', - 'azureFunctions.templateFilter': 'Core', - 'azureFunctions.showTargetFrameworkWarning': false, - 'azureFunctions.projectSubpath': `bin\\Release\\${targetFramework ?? TargetFramework.NetFx}\\publish`, - }; - await fs.writeJson(filePath, content, { spaces: 2 }); - } - - /** - * Generates the tasks.json file in the specified folder. - * @param folderPath The path to the folder where the tasks.json file should be generated. - */ - private async generateTasksJson(folderPath: string): Promise { - const filePath = path.join(folderPath, tasksFileName); - const content = { - version: '2.0.0', - tasks: [ - { - label: 'build', - command: '${config:azureLogicAppsStandard.dotnetBinaryPath}', - type: 'process', - args: ['build', '${workspaceFolder}'], - group: { - kind: 'build', - isDefault: true, - }, - }, - ], - }; - await fs.writeJson(filePath, content, { spaces: 2 }); - } -} diff --git a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/functionAppNameStep.ts b/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/functionAppNameStep.ts deleted file mode 100644 index f8637af7b5c..00000000000 --- a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/functionAppNameStep.ts +++ /dev/null @@ -1,54 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import { ext } from '../../../../extensionVariables'; -import { localize } from '../../../../localize'; -import * as vscode from 'vscode'; -import * as fse from 'fs-extra'; -import { AzureWizardPromptStep } from '@microsoft/vscode-azext-utils'; -import type { IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps'; - -export class FunctionAppNameStep extends AzureWizardPromptStep { - public hideStepCount = true; - - public shouldPrompt(): boolean { - return true; - } - - public async prompt(context: IProjectWizardContext): Promise { - context.functionAppName = await context.ui.showInputBox({ - placeHolder: localize('setFunctionName', 'Function name'), - prompt: localize('functionNamePrompt', 'Provide a function name for functions app project'), - validateInput: async (input: string): Promise => await this.validateFunctionName(input, context), - }); - - ext.outputChannel.appendLog(localize('functionAppNameSet', `Function App project name set to ${context.functionAppName}`)); - } - - private async validateFunctionName(name: string | undefined, context: IProjectWizardContext): Promise { - if (!name || name.length === 0) { - return localize('emptyFunctionNameError', "Can't have an empty function name."); - } - - if (!/^[a-z][a-z\d_]*$/i.test(name)) { - return localize( - 'functionNameInvalidMessage', - 'The function name must start with a letter and can only contain letters, digits, or underscores ("_").' - ); - } - - if (name === context.logicAppName) { - return localize('functionNameSameAsProjectNameError', `Can't use the same name for the function and the logic app project.`); - } - - if (fse.existsSync(context.workspaceFilePath)) { - const workspaceFileContent = await vscode.workspace.fs.readFile(vscode.Uri.file(context.workspaceFilePath)); - const workspaceFileJson = JSON.parse(workspaceFileContent.toString()); - - if (workspaceFileJson.folders && workspaceFileJson.folders.some((folder: { name: string }) => folder.name === name)) { - return localize('functionNameExistsInWorkspaceError', 'A function with this name already exists in the workspace.'); - } - } - } -} diff --git a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/functionAppNamespaceStep.ts b/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/functionAppNamespaceStep.ts deleted file mode 100644 index 4585feb9cf2..00000000000 --- a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/functionAppNamespaceStep.ts +++ /dev/null @@ -1,42 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import { namespaceValidation } from '../../../../constants'; -import { ext } from '../../../../extensionVariables'; -import { localize } from '../../../../localize'; -import { AzureWizardPromptStep } from '@microsoft/vscode-azext-utils'; -import type { IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps'; - -export class FunctionAppNamespaceStep extends AzureWizardPromptStep { - public hideStepCount = true; - - public shouldPrompt(_context: IProjectWizardContext): boolean { - return true; - } - - public async prompt(context: IProjectWizardContext): Promise { - context.functionAppNamespace = await context.ui.showInputBox({ - placeHolder: localize('setNamespace', 'Namespace'), - prompt: localize('methodNamePrompt', 'Provide a namespace for functions project'), - validateInput: async (input: string): Promise => await this.validateNamespace(input), - }); - - ext.outputChannel.appendLog( - localize('functionAppNamespaceSet', `Function App project namespace set to ${context.functionAppNamespace}`) - ); - } - - private async validateNamespace(namespace: string | undefined): Promise { - if (!namespace) { - return localize('emptyNamespaceError', `Can't have an empty namespace.`); - } - - if (!namespaceValidation.test(namespace)) { - return localize( - 'namespaceInvalidMessage', - 'The namespace must start with a letter or underscore, contain only letters, digits, underscores, and periods, and must not end with a period.' - ); - } - } -} diff --git a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/initCustomCodeProjectStep.ts b/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/initCustomCodeProjectStep.ts deleted file mode 100644 index 76653ddd51d..00000000000 --- a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/initCustomCodeProjectStep.ts +++ /dev/null @@ -1,53 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import { binariesExistSync } from '../../../utils/binaries'; -import { getFuncHostTaskEnv } from '../../../utils/codeless/funcHostTaskEnv'; -import { extensionCommand, func, funcDependencyName, funcWatchProblemMatcher, hostStartCommand } from '../../../../constants'; -import { InitCustomCodeScriptProjectStep } from './initCustomCodeScriptProjectStep'; -import type { ITaskInputs, ISettingToAdd } from '@microsoft/vscode-extension-logic-apps'; -import type { TaskDefinition } from 'vscode'; - -export class InitCustomCodeProjectStep extends InitCustomCodeScriptProjectStep { - protected getTasks(): TaskDefinition[] { - const funcBinariesExist = binariesExistSync(funcDependencyName); - const binariesOptions = funcBinariesExist ? getFuncHostTaskEnv() : {}; - return [ - { - label: 'generateDebugSymbols', - command: '${config:azureLogicAppsStandard.dotnetBinaryPath}', - args: ['${input:getDebugSymbolDll}'], - type: 'process', - problemMatcher: '$msCompile', - }, - { - 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, - }, - }, - ]; - } - - protected getTaskInputs(): ITaskInputs[] { - return [ - { - id: 'getDebugSymbolDll', - type: 'command', - command: extensionCommand.getDebugSymbolDll, - }, - ]; - } - - protected getWorkspaceSettings(): ISettingToAdd[] { - return [{ prefix: 'azureFunctions', key: 'suppressProject', value: true }]; - } -} diff --git a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/initCustomCodeProjectStepBase.ts b/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/initCustomCodeProjectStepBase.ts deleted file mode 100644 index 022ff34a04b..00000000000 --- a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/initCustomCodeProjectStepBase.ts +++ /dev/null @@ -1,371 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import { - func, - projectLanguageSetting, - funcVersionSetting, - tasksVersion, - tasksFileName, - launchVersion, - settingsFileName, - preDeployTaskSetting, - launchFileName, - extensionsFileName, - logicAppsStandardExtensionId, - vscodeFolderName, - gitignoreFileName, -} from '../../../../constants'; -import { ext } from '../../../../extensionVariables'; -import { localize } from '../../../../localize'; -import { - type CustomCodeFunctionsProjectMetadata, - getCustomCodeFunctionsProjectMetadata, - tryGetLogicAppCustomCodeFunctionsProjects, -} from '../../../utils/customCodeUtils'; -import { getDebugConfiguration } from '../../../utils/debug'; -import { getFuncHostTaskEnv } from '../../../utils/codeless/funcHostTaskEnv'; -import { isSubpath, confirmEditJsonFile, confirmOverwriteFile } from '../../../utils/fs'; -import { tryGetLogicAppProjectRoot } from '../../../utils/verifyIsProject'; -import { - getDebugConfigs, - getLaunchVersion, - isDebugConfigEqual, - updateDebugConfigs, - updateLaunchVersion, -} from '../../../utils/vsCodeConfig/launch'; -import { updateWorkspaceSetting } from '../../../utils/vsCodeConfig/settings'; -import { getTasksVersion, updateTasksVersion, updateTasks, getTasks, updateInputs, getInputs } from '../../../utils/vsCodeConfig/tasks'; -import { isMultiRootWorkspace } from '../../../utils/workspace'; -import { AzureWizardExecuteStep, nonNullProp } from '@microsoft/vscode-azext-utils'; -import type { IActionContext } from '@microsoft/vscode-azext-utils'; -import type { - ISettingToAdd, - IProjectWizardContext, - ProjectLanguage, - ITask, - ITaskInputs, - ITasksJson, - ILaunchJson, - IExtensionsJson, - FuncVersion, - TargetFramework, -} from '@microsoft/vscode-extension-logic-apps'; -import { WorkflowProjectType } from '@microsoft/vscode-extension-logic-apps'; -import * as fse from 'fs-extra'; -import * as path from 'path'; -import type { TaskDefinition, DebugConfiguration, WorkspaceFolder } from 'vscode'; - -export abstract class InitCustomCodeProjectStepBase extends AzureWizardExecuteStep { - public priority = 20; - protected preDeployTask?: string; - protected settings: ISettingToAdd[] = []; - - protected abstract executeCore(context: IProjectWizardContext): Promise; - protected abstract getTasks(): TaskDefinition[]; - protected getTaskInputs?(): ITaskInputs[]; - protected getWorkspaceSettings?(): ISettingToAdd[]; - - protected getRecommendedExtensions?(language: ProjectLanguage): string[]; - - public shouldExecute(_context: IProjectWizardContext): boolean { - return true; - } - - public async execute(context: IProjectWizardContext): Promise { - await this.executeCore(context); - - const version: FuncVersion = nonNullProp(context, 'version'); - context.telemetry.properties.projectRuntime = version; - - const language: ProjectLanguage = nonNullProp(context, 'language'); - context.telemetry.properties.projectLanguage = language; - - context.telemetry.properties.isProjectInSubDir = String(isSubpath(context.workspacePath, context.projectPath)); - // Define the Logic App folder path using the context property of the wizard - const logicAppFolderPath = context.logicAppFolderPath; - - // Create the necessary files and folders for Visual Studio Code under the logic app folder path. - await fse.ensureDir(logicAppFolderPath); - const vscodePath: string = path.join(logicAppFolderPath, vscodeFolderName); - await fse.ensureDir(vscodePath); - - // Write the necessary Visual Studio Code configuration files. - await this.writeTasksJson(context, vscodePath); - await this.writeLaunchJson( - context, - context.workspaceFolder, - vscodePath, - version, - context.logicAppName || context.workspaceFolder.name, - context.targetFramework - ); - await this.writeSettingsJson(context, vscodePath, language, version); - await this.writeExtensionsJson(context, vscodePath, language); - - // Remove '.vscode' from gitignore if applicable - const gitignorePath: string = path.join(context.projectPath, gitignoreFileName); - if (await fse.pathExists(gitignorePath)) { - let gitignoreContents: string = (await fse.readFile(gitignorePath)).toString(); - gitignoreContents = gitignoreContents.replace(/^\.vscode(\/|\\)?\s*$/gm, ''); - await fse.writeFile(gitignorePath, gitignoreContents); - } - } - - /** - * Overwrites the tasks.json file with the specified content. - * @param context The project wizard context. - **/ - public async overwriteTasksJson(context: IProjectWizardContext): Promise { - const tasksJsonPath: string = path.join(context.projectPath, vscodeFolderName, tasksFileName); - const 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'], - ...getFuncHostTaskEnv(), - problemMatcher: '$func-watch', - isBackground: true, - label: 'func: host start', - group: { - kind: 'build', - isDefault: true, - }, - }, - ], - inputs: [ - { - id: 'getDebugSymbolDll', - type: 'command', - command: 'azureLogicAppsStandard.getDebugSymbolDll', - }, - ], - }; - - if (await confirmOverwriteFile(context, tasksJsonPath)) { - await fse.writeFile(tasksJsonPath, JSON.stringify(tasksJsonContent, null, 2)); - } - } - - protected addSubDir(context: IProjectWizardContext, fsPath: string): string { - const subDir: string = path.relative(context.workspacePath, context.projectPath); - // always use posix for debug config - return path.posix.join(subDir, fsPath); - } - - private async writeTasksJson(context: IProjectWizardContext, vscodePath: string): Promise { - const newTasks: TaskDefinition[] = this.getTasks(); - const versionMismatchError: Error = new Error( - localize('versionMismatchError', 'The version in your {0} must be "{1}" to work with Azure Functions.', tasksFileName, tasksVersion) - ); - - // Use the Visual Studio Code API to update config, if the folder is open and isn't a multi-root workspace (https://github.com/Microsoft/vscode-azurefunctions/issues/1235). - // The Visual Studio Code API is better for several reasons: - // - The Visual Studio Code API handles comments in JSON files. - // - The Visual Studio Code API sends the 'onDidChangeConfiguration' event. - if (context.workspaceFolder && !isMultiRootWorkspace()) { - const currentVersion: string | undefined = getTasksVersion(context.workspaceFolder); - if (!currentVersion) { - updateTasksVersion(context.workspaceFolder, tasksVersion); - } else if (currentVersion !== tasksVersion) { - throw versionMismatchError; - } - updateTasks(context.workspaceFolder, this.insertNewTasks(getTasks(context.workspaceFolder), newTasks)); - if (this.getTaskInputs) { - updateInputs(context.workspaceFolder, this.insertNewTaskInputs(context, getInputs(context.workspaceFolder), this.getTaskInputs())); - } - } else { - // otherwise manually edit json - const tasksJsonPath: string = path.join(vscodePath, tasksFileName); - await confirmEditJsonFile(context, tasksJsonPath, (data: ITasksJson): ITasksJson => { - if (!data.version) { - data.version = tasksVersion; - } else if (data.version !== tasksVersion) { - throw versionMismatchError; - } - data.tasks = this.insertNewTasks(data.tasks, newTasks); - if (this.getTaskInputs) { - data.inputs = this.insertNewTaskInputs(context, data.inputs, this.getTaskInputs()); - } - return data; - }); - } - } - - private insertNewTasks(existingTasks: ITask[] | undefined, newTasks: ITask[]): ITask[] { - // tslint:disable-next-line: strict-boolean-expressions - existingTasks = existingTasks || []; - // Remove tasks that match the ones we're about to add - existingTasks = existingTasks.filter( - (t1) => - !newTasks.find((t2) => { - if (t1.type === t2.type) { - switch (t1.type) { - case func: - return t1.command === t2.command; - case 'shell': - case 'process': - return t1.label === t2.label && t1.identifier === t2.identifier; - default: - // Not worth throwing an error for unrecognized task type - // Worst case the user has an extra task in their tasks.json - return false; - } - } - return false; - }) - ); - existingTasks.push(...newTasks); - return existingTasks; - } - - private insertNewTaskInputs(context: IProjectWizardContext, existingInputs: ITaskInputs[] = [], newInputs: ITaskInputs[]): ITaskInputs[] { - if (context.workflowProjectType === WorkflowProjectType.Bundle) { - // Remove inputs that match the ones we're about to add - existingInputs = existingInputs.filter( - (t1) => - !newInputs.find((t2) => { - if (t1.type === t2.type) { - switch (t1.type) { - case 'command': - return t1.command === t2.command; - default: - return false; - } - } - return false; - }) - ); - existingInputs.push(...newInputs); - } - return existingInputs; - } - - private async writeLaunchJson( - context: IActionContext, - folder: WorkspaceFolder | undefined, - vscodePath: string, - version: FuncVersion, - logicAppName: string, - customCodeTargetFramework?: TargetFramework - ): Promise { - if (!customCodeTargetFramework) { - const logicAppFolderPath = await tryGetLogicAppProjectRoot(context, folder); - const customCodeProjectPaths = await tryGetLogicAppCustomCodeFunctionsProjects(logicAppFolderPath); - let customCodeProjectsMetadata: CustomCodeFunctionsProjectMetadata[]; - if (customCodeProjectPaths && customCodeProjectPaths.length > 0) { - customCodeProjectsMetadata = await Promise.all(customCodeProjectPaths.map(getCustomCodeFunctionsProjectMetadata)); - } - // Currently only support one custom code functions project per logic app - customCodeTargetFramework = customCodeProjectsMetadata ? customCodeProjectsMetadata[0].targetFramework : undefined; - } - - const newDebugConfig: DebugConfiguration = getDebugConfiguration(version, logicAppName, customCodeTargetFramework); - const versionMismatchError: Error = new Error( - localize('versionMismatchError', 'The version in your {0} must be "{1}" to work with Azure Functions.', launchFileName, launchVersion) - ); - - // Use the Visual Studio Code API to update config, if the folder is open and isn't a multi-root workspace (https://github.com/Microsoft/vscode-azurefunctions/issues/1235). - // The Visual Studio Code API is better for several reasons: - // - The API handles comments in JSON files. - // - The API sends the 'onDidChangeConfiguration' event. - if (folder && !isMultiRootWorkspace()) { - const currentVersion: string | undefined = getLaunchVersion(folder); - if (!currentVersion) { - updateLaunchVersion(folder, launchVersion); - } else if (currentVersion !== launchVersion) { - throw versionMismatchError; - } - updateDebugConfigs(folder, this.insertLaunchConfig(getDebugConfigs(folder), newDebugConfig)); - } else { - // otherwise manually edit json - const launchJsonPath: string = path.join(vscodePath, launchFileName); - await confirmEditJsonFile(context, launchJsonPath, (data: ILaunchJson): ILaunchJson => { - if (!data.version) { - data.version = launchVersion; - } else if (data.version !== launchVersion) { - throw versionMismatchError; - } - data.configurations = this.insertLaunchConfig(data.configurations, newDebugConfig); - return data; - }); - } - } - - private insertLaunchConfig(existingConfigs: DebugConfiguration[] | undefined, newConfig: DebugConfiguration): DebugConfiguration[] { - // tslint:disable-next-line: strict-boolean-expressions - existingConfigs = existingConfigs || []; - // Remove configs that match the one we're about to add - existingConfigs = existingConfigs.filter((l1) => !isDebugConfigEqual(l1, newConfig)); - existingConfigs.push(newConfig); - return existingConfigs; - } - - private async writeSettingsJson( - context: IProjectWizardContext, - vscodePath: string, - language: string, - version: FuncVersion - ): Promise { - const settings: ISettingToAdd[] = this.settings.concat( - { key: projectLanguageSetting, value: language }, - { key: funcVersionSetting, value: version }, - // We want the terminal to open after F5, not the debug console because HTTP triggers are printed in the terminal. - { prefix: 'debug', key: 'internalConsoleOptions', value: 'neverOpen' } - ); - - if (this.preDeployTask) { - settings.push({ key: preDeployTaskSetting, value: this.preDeployTask }); - } - - if (this.getWorkspaceSettings) { - const settingsToAdd = this.getWorkspaceSettings(); - settings.push(...settingsToAdd); - } - - if (context.workspaceFolder) { - // Use Visual Studio Code API to update config if folder is open - for (const setting of settings) { - await updateWorkspaceSetting(setting.key, setting.value, context.workspacePath, setting.prefix); - } - } else { - // otherwise manually edit json - const settingsJsonPath: string = path.join(vscodePath, settingsFileName); - await confirmEditJsonFile(context, settingsJsonPath, (data: Record): Record => { - for (const setting of settings) { - const key = `${setting.prefix || ext.prefix}.${setting.key}`; - data[key] = setting.value; - } - return data; - }); - } - } - - private async writeExtensionsJson(context: IActionContext, vscodePath: string, language: ProjectLanguage): Promise { - const extensionsJsonPath: string = path.join(vscodePath, extensionsFileName); - await confirmEditJsonFile(context, extensionsJsonPath, (data: IExtensionsJson): Record => { - const recommendations: string[] = [logicAppsStandardExtensionId]; - if (this.getRecommendedExtensions) { - recommendations.push(...this.getRecommendedExtensions(language)); - } - - if (data.recommendations) { - recommendations.push(...data.recommendations); - } - - // de-dupe array - data.recommendations = recommendations.filter((rec: string, index: number) => recommendations.indexOf(rec) === index); - return data; - }); - } -} diff --git a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/initCustomCodeScriptProjectStep.ts b/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/initCustomCodeScriptProjectStep.ts deleted file mode 100644 index 883c92c8c04..00000000000 --- a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/initCustomCodeScriptProjectStep.ts +++ /dev/null @@ -1,67 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import { binariesExistSync } from '../../../utils/binaries'; -import { getFuncHostTaskEnv } from '../../../utils/codeless/funcHostTaskEnv'; -import { extInstallTaskName, func, funcDependencyName, funcWatchProblemMatcher, hostStartCommand } from '../../../../constants'; -import { getLocalFuncCoreToolsVersion } from '../../../utils/funcCoreTools/funcVersion'; -import { InitCustomCodeProjectStepBase } from './initCustomCodeProjectStepBase'; -import type { IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps'; -import { FuncVersion } from '@microsoft/vscode-extension-logic-apps'; -import * as fse from 'fs-extra'; -import * as path from 'path'; -import * as semver from 'semver'; -import type { TaskDefinition } from 'vscode'; - -/** - * Base class for all projects based on a simple script (i.e. JavaScript, C# Script, Bash, etc.) that don't require compilation - */ -export class InitCustomCodeScriptProjectStep extends InitCustomCodeProjectStepBase { - protected useFuncExtensionsInstall = false; - - protected async executeCore(context: IProjectWizardContext): Promise { - try { - const extensionsCsprojPath: string = path.join(context.projectPath, 'extensions.csproj'); - - if (await fse.pathExists(extensionsCsprojPath)) { - this.useFuncExtensionsInstall = true; - context.telemetry.properties.hasExtensionsCsproj = 'true'; - } else if (context.version === FuncVersion.v2) { - // no need to check v1 or v3+ - const currentVersion: string | null = await getLocalFuncCoreToolsVersion(); - // Starting after this version, projects can use extension bundle instead of running "func extensions install" - this.useFuncExtensionsInstall = !!currentVersion && semver.lte(currentVersion, '2.5.553'); - } - } catch { - // use default of false - } - - if (this.useFuncExtensionsInstall) { - // "func extensions install" task creates C# build artifacts that should be hidden - // See issue: https://github.com/Microsoft/vscode-azurefunctions/pull/699 - this.settings.push({ prefix: 'files', key: 'exclude', value: { obj: true, bin: true } }); - - if (!this.preDeployTask) { - this.preDeployTask = extInstallTaskName; - } - } - } - - protected getTasks(): TaskDefinition[] { - const funcBinariesExist = binariesExistSync(funcDependencyName); - const binariesOptions = funcBinariesExist ? getFuncHostTaskEnv() : {}; - return [ - { - 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/createProject/createCustomCodeProjectSteps/targetFrameworkStep.ts b/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/targetFrameworkStep.ts deleted file mode 100644 index 6b40d800c83..00000000000 --- a/apps/vs-code-designer/src/app/commands/createProject/createCustomCodeProjectSteps/targetFrameworkStep.ts +++ /dev/null @@ -1,39 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import { type IProjectWizardContext, TargetFramework, ProjectType, Platform } from '@microsoft/vscode-extension-logic-apps'; -import { AzureWizardPromptStep, type IAzureQuickPickItem } from '@microsoft/vscode-azext-utils'; -import { localize } from '../../../../localize'; - -/** - * Represents a step in the project creation wizard for selecting the target framework. - */ -export class TargetFrameworkStep extends AzureWizardPromptStep { - public hideStepCount = true; - - /** - * Determines whether this step should be prompted based on the project wizard context. - * @param {IProjectWizardContext} context - The project wizard context. - * @returns True if this step should be prompted, false otherwise. - */ - public shouldPrompt(context: IProjectWizardContext): boolean { - return context.projectType === ProjectType.customCode; - } - - /** - * Prompts the user to select a target framework. - * @param {IProjectWizardContext} context - The project wizard context. - */ - public async prompt(context: IProjectWizardContext): Promise { - const placeHolder: string = localize('selectTargetFramework', 'Select a target framework.'); - const picks: IAzureQuickPickItem[] = [ - { label: localize('Net8', '.NET 8'), data: TargetFramework.Net8 }, - { label: localize('Net10', '.NET 10'), data: TargetFramework.Net10 }, - ]; - if (process.platform === Platform.windows) { - picks.unshift({ label: localize('NetFx', '.NET Framework'), data: TargetFramework.NetFx }); - } - context.targetFramework = (await context.ui.showQuickPick(picks, { placeHolder })).data; - } -} diff --git a/apps/vs-code-designer/src/app/commands/enableDevContainer/__test__/enableDevContainerIntegration.test.ts b/apps/vs-code-designer/src/app/commands/enableDevContainer/__test__/enableDevContainerIntegration.test.ts index 5f14b421a28..9c3936ed51a 100644 --- a/apps/vs-code-designer/src/app/commands/enableDevContainer/__test__/enableDevContainerIntegration.test.ts +++ b/apps/vs-code-designer/src/app/commands/enableDevContainer/__test__/enableDevContainerIntegration.test.ts @@ -55,6 +55,15 @@ describe('enableDevContainer - Integration Tests', () => { vi.spyOn(vscode.window, 'showInformationMessage').mockResolvedValue(undefined); vi.spyOn(vscode.window, 'showErrorMessage').mockResolvedValue(undefined); vi.spyOn(vscode.window, 'showWarningMessage').mockResolvedValue(undefined as any); + + // Ensure workspace.getConfiguration returns a proper mock object + // (needed by binariesExistSync which is called during tasks.json generation) + vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({ + get: vi.fn(), + has: vi.fn().mockReturnValue(false), + inspect: vi.fn(), + update: vi.fn().mockResolvedValue(undefined), + } as any); }); afterEach(async () => { @@ -204,18 +213,19 @@ describe('enableDevContainer - Integration Tests', () => { expect(convertedTasks.tasks).toBeDefined(); expect(convertedTasks.inputs).toBeDefined(); - // Verify devcontainer-specific configuration paths + // Verify devcontainer-specific configuration const funcHostStartTask = convertedTasks.tasks.find((task: any) => task.label === 'func: host start'); expect(funcHostStartTask).toBeDefined(); - expect(funcHostStartTask.command).toContain('${config:azureLogicAppsStandard.funcCoreToolsBinaryPath}'); const generateDebugTask = convertedTasks.tasks.find((task: any) => task.label === 'generateDebugSymbols'); expect(generateDebugTask).toBeDefined(); - expect(generateDebugTask.command).toContain('${config:azureLogicAppsStandard.dotnetBinaryPath}'); - // Verify options are removed (devcontainer manages PATH) + // Verify platform-keyed env options are removed (devcontainer manages PATH) convertedTasks.tasks.forEach((task: any) => { expect(task.options).toBeUndefined(); + expect(task.windows).toBeUndefined(); + expect(task.linux).toBeUndefined(); + expect(task.osx).toBeUndefined(); }); }); @@ -256,16 +266,20 @@ describe('enableDevContainer - Integration Tests', () => { const tasks1 = await fse.readJson(tasksJson1Path); const tasks2 = await fse.readJson(tasksJson2Path); - // Both should have devcontainer-compatible paths - expect(tasks1.tasks[1].command).toContain('${config:azureLogicAppsStandard.funcCoreToolsBinaryPath}'); - expect(tasks2.tasks[1].command).toContain('${config:azureLogicAppsStandard.funcCoreToolsBinaryPath}'); + // Both should be devcontainer-compatible (no platform-keyed env) + const funcTask1 = tasks1.tasks.find((t: any) => t.label === 'func: host start'); + const funcTask2 = tasks2.tasks.find((t: any) => t.label === 'func: host start'); + expect(funcTask1).toBeDefined(); + expect(funcTask2).toBeDefined(); + expect(funcTask1.options).toBeUndefined(); + expect(funcTask2.options).toBeUndefined(); - // Verify options are removed from both Logic Apps (devcontainer manages PATH) + // Verify platform-keyed env options are removed from both Logic Apps (devcontainer manages PATH) tasks1.tasks.forEach((task: any) => { - expect(task.options).toBeUndefined(); + expect(task.windows).toBeUndefined(); }); tasks2.tasks.forEach((task: any) => { - expect(task.options).toBeUndefined(); + expect(task.windows).toBeUndefined(); }); // Verify telemetry shows 2 tasks were converted @@ -509,12 +523,11 @@ describe('enableDevContainer - Integration Tests', () => { // Verify devcontainer-specific paths const funcHostStartTask = tasksContent.tasks.find((task: any) => task.label === 'func: host start'); expect(funcHostStartTask).toBeDefined(); - expect(funcHostStartTask.command).toContain('${config:azureLogicAppsStandard.funcCoreToolsBinaryPath}'); - - // Verify options are not present (devcontainer manages PATH) - tasksContent.tasks.forEach((task: any) => { - expect(task.options).toBeUndefined(); - }); + // In devcontainer mode, platform-keyed env options should be absent regardless of binary availability + expect(funcHostStartTask.options).toBeUndefined(); + expect(funcHostStartTask.windows).toBeUndefined(); + expect(funcHostStartTask.linux).toBeUndefined(); + expect(funcHostStartTask.osx).toBeUndefined(); }); it('should use regular tasks.json template when workspace has no devcontainer', async () => { @@ -544,21 +557,19 @@ describe('enableDevContainer - Integration Tests', () => { await createLogicAppProject(mockContext, options, workspaceRootFolder); - // Verify the second logic app's tasks.json uses regular paths with options + // Verify the second logic app's tasks.json uses correct structure const tasksJsonPath = path.join(secondLogicAppPath, vscodeFolderName, tasksFileName); expect(await fse.pathExists(tasksJsonPath)).toBe(true); const tasksContent = await fse.readJson(tasksJsonPath); - // Verify regular paths + // Verify func: host start task exists with correct structure const funcHostStartTask = tasksContent.tasks.find((task: any) => task.label === 'func: host start'); expect(funcHostStartTask).toBeDefined(); - expect(funcHostStartTask.command).toContain('${config:azureLogicAppsStandard.funcCoreToolsBinaryPath}'); - - // Verify options ARE present for non-devcontainer setup - expect(funcHostStartTask.options).toBeDefined(); - expect(funcHostStartTask.options.env).toBeDefined(); - expect(funcHostStartTask.options.env.PATH).toBeDefined(); + expect(funcHostStartTask.isBackground).toBe(true); + // Non-devcontainer projects get the binary path command when binaries are installed, + // or the 'host start' command (type: func) when they aren't. Both are valid. + expect(funcHostStartTask.command).toBeDefined(); }); }); }); diff --git a/apps/vs-code-designer/src/app/commands/enableDevContainer/enableDevContainer.ts b/apps/vs-code-designer/src/app/commands/enableDevContainer/enableDevContainer.ts index dbfd27f0406..b87eded6c32 100644 --- a/apps/vs-code-designer/src/app/commands/enableDevContainer/enableDevContainer.ts +++ b/apps/vs-code-designer/src/app/commands/enableDevContainer/enableDevContainer.ts @@ -4,12 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from '../../../localize'; import { ext } from '../../../extensionVariables'; -import { devContainerFileName, devContainerFolderName, tasksFileName, vscodeFolderName } from '../../../constants'; +import { devContainerFileName, devContainerFolderName, funcDependencyName, tasksFileName, vscodeFolderName } from '../../../constants'; import type { IActionContext } from '@microsoft/vscode-azext-utils'; import * as vscode from 'vscode'; import * as fse from 'fs-extra'; import * as path from 'path'; -import { getContainerTemplatePath, getWorkspaceTemplatePath } from '../../utils/assets'; +import { getContainerTemplatePath } from '../../utils/assets'; +import { binariesExistSync } from '../../utils/binaries'; +import { detectProjectType, detectProjectPackageType } from '../../utils/project'; +import { generateTasksJson } from '../../utils/vsCodeConfig/generators'; /** * Enables devcontainer support for an existing Logic App workspace @@ -150,24 +153,18 @@ async function convertWorkspaceTasksToDevContainer(context: IActionContext, work * @param tasksJsonPath - Path to the tasks.json file */ async function convertTasksJsonToDevContainer(context: IActionContext, tasksJsonPath: string): Promise { - const tasksContent = await fse.readJson(tasksJsonPath); - - // Get the devcontainer-compatible template - const devContainerTasksTemplatePath = getWorkspaceTemplatePath('DevContainerTasksJsonFile'); - - if (!(await fse.pathExists(devContainerTasksTemplatePath))) { - throw new Error(localize('templateNotFound', 'DevContainer tasks template not found at: {0}', devContainerTasksTemplatePath)); - } - - const devContainerTasksTemplate = await fse.readJson(devContainerTasksTemplatePath); - - // Replace the tasks with devcontainer-compatible versions - // Keep the version, but update tasks and inputs - tasksContent.tasks = devContainerTasksTemplate.tasks; - tasksContent.inputs = devContainerTasksTemplate.inputs; - - // Write the updated tasks.json - await fse.writeJson(tasksJsonPath, tasksContent, { spaces: 2 }); + const logicAppPath = path.dirname(path.dirname(tasksJsonPath)); + const projectType = await detectProjectType(logicAppPath); + const projectPackageType = await detectProjectPackageType(logicAppPath); + + const tasksJsonContent = generateTasksJson({ + projectType, + projectPackageType, + hasFuncBinaries: binariesExistSync(funcDependencyName), + isDevContainer: true, + }); + + await fse.writeJson(tasksJsonPath, tasksJsonContent, { spaces: 2 }); } /** diff --git a/apps/vs-code-designer/src/app/commands/initProjectForVSCode/initDotnetProjectStep.ts b/apps/vs-code-designer/src/app/commands/initProjectForVSCode/initDotnetProjectStep.ts index ffbe0748a56..57dd7fdb907 100644 --- a/apps/vs-code-designer/src/app/commands/initProjectForVSCode/initDotnetProjectStep.ts +++ b/apps/vs-code-designer/src/app/commands/initProjectForVSCode/initDotnetProjectStep.ts @@ -2,19 +2,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { - dotnetPublishTaskLabel, - funcDependencyName, - dotnetExtensionId, - func, - funcWatchProblemMatcher, - hostStartCommand, - show64BitWarningSetting, -} from '../../../constants'; +import { dotnetPublishTaskLabel, dotnetExtensionId, show64BitWarningSetting } from '../../../constants'; import { localize } from '../../../localize'; -import { binariesExistSync } from '../../utils/binaries'; -import { getFuncHostTaskEnv } from '../../utils/codeless/funcHostTaskEnv'; -import { getProjFiles, getTargetFramework, getDotnetDebugSubpath, tryGetFuncVersion } from '../../utils/dotnet/dotnet'; +import { getProjFiles, getTargetFramework, tryGetFuncVersion } from '../../utils/dotnet/dotnet'; import type { ProjectFile } from '../../utils/dotnet/dotnet'; import { tryParseFuncVersion } from '../../utils/funcCoreTools/funcVersion'; import { getWorkspaceSetting, updateGlobalSetting } from '../../utils/vsCodeConfig/settings'; @@ -23,11 +13,10 @@ import { DialogResponses, nonNullProp, openUrl, parseError } from '@microsoft/vs import { FuncVersion, ProjectLanguage } from '@microsoft/vscode-extension-logic-apps'; import type { IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps'; import * as path from 'path'; -import type { MessageItem, TaskDefinition } from 'vscode'; +import type { MessageItem } from 'vscode'; export class InitDotnetProjectStep extends InitProjectStepBase { protected preDeployTask: string = dotnetPublishTaskLabel; - private debugSubpath: string; protected getRecommendedExtensions(language: ProjectLanguage): string[] { const recs: string[] = [dotnetExtensionId]; @@ -101,61 +90,6 @@ export class InitDotnetProjectStep extends InitProjectStepBase { } } - const targetFramework: string = await getTargetFramework(projFile); - await this.setDeploySubpath(context, path.posix.join('bin', 'Release', targetFramework, 'publish')); - this.debugSubpath = getDotnetDebugSubpath(targetFramework); - } - - protected getTasks(): TaskDefinition[] { - const commonArgs: string[] = ['/property:GenerateFullPaths=true', '/consoleloggerparameters:NoSummary']; - const releaseArgs: string[] = ['--configuration', 'Release']; - const funcBinariesExist = binariesExistSync(funcDependencyName); - const binariesOptions = funcBinariesExist ? getFuncHostTaskEnv({ cwd: this.debugSubpath }) : {}; - return [ - { - 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: ['clean', ...releaseArgs, ...commonArgs], - type: 'process', - problemMatcher: '$msCompile', - }, - { - label: dotnetPublishTaskLabel, - command: '${config:azureLogicAppsStandard.dotnetBinaryPath}', - args: ['publish', ...releaseArgs, ...commonArgs], - type: 'process', - dependsOn: 'clean release', - problemMatcher: '$msCompile', - }, - { - label: 'func: host start', - type: funcBinariesExist ? 'shell' : func, - dependsOn: 'build', - ...binariesOptions, - command: funcBinariesExist ? '${config:azureLogicAppsStandard.funcCoreToolsBinaryPath}' : hostStartCommand, - args: funcBinariesExist ? ['host', 'start'] : undefined, - isBackground: true, - problemMatcher: funcWatchProblemMatcher, - }, - ]; + context.targetFramework = await getTargetFramework(projFile); } } diff --git a/apps/vs-code-designer/src/app/commands/initProjectForVSCode/initProjectLanguageStep.ts b/apps/vs-code-designer/src/app/commands/initProjectForVSCode/initProjectLanguageStep.ts index c42bb3c6662..960519fd9fa 100644 --- a/apps/vs-code-designer/src/app/commands/initProjectForVSCode/initProjectLanguageStep.ts +++ b/apps/vs-code-designer/src/app/commands/initProjectForVSCode/initProjectLanguageStep.ts @@ -3,14 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { localize } from '../../../localize'; -import { InitCustomCodeProjectStep } from '../createProject/createCustomCodeProjectSteps/initCustomCodeProjectStep'; import { InitDotnetProjectStep } from './initDotnetProjectStep'; import { InitProjectStep } from './initProjectStep'; -import { isEmptyString } from '@microsoft/logic-apps-shared'; import { AzureWizardPromptStep } from '@microsoft/vscode-azext-utils'; import type { AzureWizardExecuteStep, IWizardOptions } from '@microsoft/vscode-azext-utils'; import type { IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps'; -import { ProjectLanguage, WorkflowProjectType } from '@microsoft/vscode-extension-logic-apps'; +import { ProjectLanguage, ProjectPackageType } from '@microsoft/vscode-extension-logic-apps'; import type { QuickPickItem, QuickPickOptions } from 'vscode'; export class InitProjectLanguageStep extends AzureWizardPromptStep { @@ -24,30 +22,29 @@ export class InitProjectLanguageStep extends AzureWizardPromptStep> { const executeSteps: AzureWizardExecuteStep[] = []; const promptSteps: AzureWizardPromptStep[] = []; - await addInitVSCodeSteps(context, executeSteps, false); + await addInitVSCodeSteps(context, executeSteps); return { promptSteps, executeSteps }; } } export async function addInitVSCodeSteps( context: IProjectWizardContext, - executeSteps: AzureWizardExecuteStep[], - isCustomCode: boolean + executeSteps: AzureWizardExecuteStep[] ): Promise { switch (context.language) { case ProjectLanguage.JavaScript: { - context.workflowProjectType = WorkflowProjectType.Bundle; - executeSteps.push(isCustomCode ? new InitCustomCodeProjectStep() : new InitProjectStep()); + context.projectPackageType = ProjectPackageType.Bundle; + executeSteps.push(new InitProjectStep()); break; } case ProjectLanguage.CSharp: { - context.workflowProjectType = WorkflowProjectType.Nuget; + context.projectPackageType = ProjectPackageType.Nuget; executeSteps.push(new InitDotnetProjectStep()); break; } diff --git a/apps/vs-code-designer/src/app/commands/initProjectForVSCode/initProjectStep.ts b/apps/vs-code-designer/src/app/commands/initProjectForVSCode/initProjectStep.ts index 65a66561a72..eab5f872a3d 100644 --- a/apps/vs-code-designer/src/app/commands/initProjectForVSCode/initProjectStep.ts +++ b/apps/vs-code-designer/src/app/commands/initProjectForVSCode/initProjectStep.ts @@ -2,52 +2,11 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { binariesExistSync } from '../../utils/binaries'; -import { getFuncHostTaskEnv } from '../../utils/codeless/funcHostTaskEnv'; -import { extensionCommand, func, funcDependencyName, funcWatchProblemMatcher, hostStartCommand } from '../../../constants'; -import { InitScriptProjectStep } from './initScriptProjectStep'; -import type { ITaskInputs, ISettingToAdd } from '@microsoft/vscode-extension-logic-apps'; -import type { TaskDefinition } from 'vscode'; +import { InitProjectStepBase } from './initProjectStepBase'; +import type { IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps'; -export class InitProjectStep extends InitScriptProjectStep { - protected getTasks(): TaskDefinition[] { - const funcBinariesExist = binariesExistSync(funcDependencyName); - const binariesOptions = funcBinariesExist ? getFuncHostTaskEnv() : {}; - return [ - { - label: 'generateDebugSymbols', - command: '${config:azureLogicAppsStandard.dotnetBinaryPath}', - args: ['${input:getDebugSymbolDll}'], - type: 'process', - problemMatcher: '$msCompile', - }, - { - 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, - }, - }, - ]; - } - - protected getTaskInputs(): ITaskInputs[] { - return [ - { - id: 'getDebugSymbolDll', - type: 'command', - command: extensionCommand.getDebugSymbolDll, - }, - ]; - } - - protected getWorkspaceSettings(): ISettingToAdd[] { - return [{ prefix: 'azureFunctions', key: 'suppressProject', value: true }]; +export class InitProjectStep extends InitProjectStepBase { + protected async executeCore(_context: IProjectWizardContext): Promise { + // Deploy subpath is handled by the settings generator } } diff --git a/apps/vs-code-designer/src/app/commands/initProjectForVSCode/initProjectStepBase.ts b/apps/vs-code-designer/src/app/commands/initProjectForVSCode/initProjectStepBase.ts index f03281688aa..c2abbb8e879 100644 --- a/apps/vs-code-designer/src/app/commands/initProjectForVSCode/initProjectStepBase.ts +++ b/apps/vs-code-designer/src/app/commands/initProjectForVSCode/initProjectStepBase.ts @@ -3,66 +3,32 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { - func, - projectLanguageSetting, - funcVersionSetting, - deploySubpathSetting, - tasksVersion, + funcDependencyName, tasksFileName, - launchVersion, - settingsFileName, - preDeployTaskSetting, launchFileName, + settingsFileName, extensionsFileName, vscodeFolderName, - logicAppsStandardExtensionId, designTimeDirectoryName, } from '../../../constants'; -import { ext } from '../../../extensionVariables'; import { localize } from '../../../localize'; -import { isSubpath, isPathEqual, confirmEditJsonFile } from '../../utils/fs'; +import { binariesExistSync } from '../../utils/binaries'; +import { detectCustomCodeTargetFramework } from '../../utils/customCodeUtils'; +import { isSubpath, writeFormattedJson } from '../../utils/fs'; import { removeFromGitIgnore } from '../../utils/git'; -import { - getDebugConfigs, - getLaunchVersion, - isDebugConfigEqual, - updateDebugConfigs, - updateLaunchVersion, -} from '../../utils/vsCodeConfig/launch'; -import { updateWorkspaceSetting } from '../../utils/vsCodeConfig/settings'; -import { getTasksVersion, updateTasksVersion, updateTasks, getTasks, updateInputs, getInputs } from '../../utils/vsCodeConfig/tasks'; -import { getWorkspaceFolder, isMultiRootWorkspace } from '../../utils/workspace'; -import { AzureWizardExecuteStep, nonNullProp } from '@microsoft/vscode-azext-utils'; -import type { IActionContext } from '@microsoft/vscode-azext-utils'; -import type { - ISettingToAdd, - IProjectWizardContext, - ProjectLanguage, - ITask, - ITaskInputs, - ITasksJson, - ILaunchJson, - IExtensionsJson, - FuncVersion, -} from '@microsoft/vscode-extension-logic-apps'; -import { WorkflowProjectType } from '@microsoft/vscode-extension-logic-apps'; +import { generateTasksJson, generateLaunchJson, generateSettingsJson, generateExtensionsJson } from '../../utils/vsCodeConfig/generators'; +import type { VSCodeProjectConfig } from '../../utils/vsCodeConfig/generators'; +import { AzureWizardExecuteStep, DialogResponses, nonNullProp } from '@microsoft/vscode-azext-utils'; +import type { IProjectWizardContext, ProjectLanguage, FuncVersion } from '@microsoft/vscode-extension-logic-apps'; +import { ProjectType, ProjectPackageType } from '@microsoft/vscode-extension-logic-apps'; import * as fse from 'fs-extra'; import * as path from 'path'; -import * as vscode from 'vscode'; -import type { TaskDefinition, DebugConfiguration, WorkspaceFolder } from 'vscode'; -import { isLogicAppProjectInRoot } from '../../utils/verifyIsProject'; import { getOrCreateDesignTimeDirectory } from '../../utils/codeless/startDesignTimeApi'; -import { getDebugConfiguration } from '../../utils/debug'; export abstract class InitProjectStepBase extends AzureWizardExecuteStep { public priority = 20; - protected preDeployTask?: string; - protected settings: ISettingToAdd[] = []; protected abstract executeCore(context: IProjectWizardContext): Promise; - protected abstract getTasks(): TaskDefinition[]; - protected getTaskInputs?(): ITaskInputs[]; - protected getWorkspaceSettings?(): ISettingToAdd[]; protected getRecommendedExtensions?(language: ProjectLanguage): string[]; @@ -83,244 +49,59 @@ export abstract class InitProjectStepBase extends AzureWizardExecuteStep { - deploySubpath = await this.addSubDir(context, deploySubpath); - this.settings.push({ key: deploySubpathSetting, value: deploySubpath }); - return deploySubpath; - } - - protected async addSubDir(context: IProjectWizardContext, fsPath: string): Promise { - let subDir = ''; - if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { - const workspaceFolder = await getWorkspaceFolder(context, undefined, true); - if (await isLogicAppProjectInRoot(workspaceFolder)) { - const projectPath = path.relative(context.workspacePath, context.projectPath); - subDir = projectPath === workspaceFolder.name ? projectPath : subDir; - } - } - // always use posix for debug config - return path.posix.join(subDir, fsPath); - } - - private async writeTasksJson(context: IProjectWizardContext, vscodePath: string): Promise { - const newTasks: TaskDefinition[] = this.getTasks(); - - for (const task of newTasks) { - let cwd: string = (task.options && task.options.cwd) || '.'; - cwd = await this.addSubDir(context, cwd); - if (!isPathEqual(cwd, '.')) { - task.options = task.options || {}; - // always use posix for debug config - task.options.cwd = path.posix.join('${workspaceFolder}', cwd); - } - } - - const versionMismatchError: Error = new Error( - localize('versionMismatchError', 'The version in your {0} must be "{1}" to work with Azure Functions.', tasksFileName, tasksVersion) - ); - - // Use VS Code api to update config if folder is open and it's not a multi-root workspace (https://github.com/Microsoft/vscode-azurefunctions/issues/1235) - // The VS Code api is better for several reasons, including: - // 1. It handles comments in json files - // 2. It sends the 'onDidChangeConfiguration' event - if (context.workspaceFolder && !isMultiRootWorkspace()) { - const currentVersion: string | undefined = getTasksVersion(context.workspaceFolder); - if (!currentVersion) { - updateTasksVersion(context.workspaceFolder, tasksVersion); - } else if (currentVersion !== tasksVersion) { - throw versionMismatchError; - } - updateTasks(context.workspaceFolder, this.insertNewTasks(getTasks(context.workspaceFolder), newTasks)); - if (this.getTaskInputs) { - updateInputs(context.workspaceFolder, this.insertNewTaskInputs(context, getInputs(context.workspaceFolder), this.getTaskInputs())); - } - } else { - // otherwise manually edit json - const tasksJsonPath: string = path.join(vscodePath, tasksFileName); - await confirmEditJsonFile(context, tasksJsonPath, (data: ITasksJson): ITasksJson => { - if (!data.version) { - data.version = tasksVersion; - } else if (data.version !== tasksVersion) { - throw versionMismatchError; - } - data.tasks = this.insertNewTasks(data.tasks, newTasks); - if (this.getTaskInputs) { - data.inputs = this.insertNewTaskInputs(context, data.inputs, this.getTaskInputs()); - } - return data; - }); - } - } - - private insertNewTasks(existingTasks: ITask[] | undefined, newTasks: ITask[]): ITask[] { - // tslint:disable-next-line: strict-boolean-expressions - existingTasks = existingTasks || []; - // Remove tasks that match the ones we're about to add - existingTasks = existingTasks.filter( - (t1) => - !newTasks.find((t2) => { - if (t1.type === t2.type) { - switch (t1.type) { - case func: - return t1.command === t2.command; - case 'shell': - case 'process': - return t1.label === t2.label && t1.identifier === t2.identifier; - default: - // Not worth throwing an error for unrecognized task type - // Worst case the user has an extra task in their tasks.json - return false; - } - } - return false; - }) - ); - existingTasks.push(...newTasks); - return existingTasks; - } - - private insertNewTaskInputs(context: IProjectWizardContext, existingInputs: ITaskInputs[] = [], newInputs: ITaskInputs[]): ITaskInputs[] { - if (context.workflowProjectType === WorkflowProjectType.Bundle) { - // Remove inputs that match the ones we're about to add - existingInputs = existingInputs.filter( - (t1) => - !newInputs.find((t2) => { - if (t1.type === t2.type) { - switch (t1.type) { - case 'command': - return t1.command === t2.command; - default: - return false; - } - } - return false; - }) - ); - existingInputs.push(...newInputs); - } - return existingInputs; - } - - private async writeLaunchJson( - context: IProjectWizardContext, - folder: WorkspaceFolder | undefined, - vscodePath: string, - version: FuncVersion, - logicAppName: string - ): Promise { - if (context.isCodeless !== false) { - const newDebugConfig: DebugConfiguration = getDebugConfiguration(version, logicAppName); - const versionMismatchError: Error = new Error( - localize( - 'versionMismatchError', - 'The version in your {0} must be "{1}" to work with Azure Functions.', - launchFileName, - launchVersion - ) + private async writeVSCodeFiles(context: IProjectWizardContext, vscodePath: string, projectConfig: VSCodeProjectConfig): Promise { + const tasksJsonPath = path.join(vscodePath, tasksFileName); + const launchJsonPath = path.join(vscodePath, launchFileName); + const settingsJsonPath = path.join(vscodePath, settingsFileName); + const extensionsJsonPath = path.join(vscodePath, extensionsFileName); + + const hasExistingFiles = + (await fse.pathExists(tasksJsonPath)) || + (await fse.pathExists(launchJsonPath)) || + (await fse.pathExists(settingsJsonPath)) || + (await fse.pathExists(extensionsJsonPath)); + + if (hasExistingFiles) { + const message = localize( + 'overwriteVSCodeFiles', + 'The .vscode configuration files will be regenerated to match the current project settings. This will overwrite any custom modifications. Continue?' ); - - // Use VS Code api to update config if folder is open and it's not a multi-root workspace (https://github.com/Microsoft/vscode-azurefunctions/issues/1235) - // The VS Code api is better for several reasons, including: - // 1. It handles comments in json files - // 2. It sends the 'onDidChangeConfiguration' event - if (folder && !isMultiRootWorkspace()) { - const currentVersion: string | undefined = getLaunchVersion(folder); - if (!currentVersion) { - updateLaunchVersion(folder, launchVersion); - } else if (currentVersion !== launchVersion) { - throw versionMismatchError; - } - updateDebugConfigs(folder, this.insertLaunchConfig(getDebugConfigs(folder), newDebugConfig)); - } else { - // otherwise manually edit json - const launchJsonPath: string = path.join(vscodePath, launchFileName); - await confirmEditJsonFile(context, launchJsonPath, (data: ILaunchJson): ILaunchJson => { - if (!data.version) { - data.version = launchVersion; - } else if (data.version !== launchVersion) { - throw versionMismatchError; - } - data.configurations = this.insertLaunchConfig(data.configurations, newDebugConfig); - return data; - }); + const result = await context.ui.showWarningMessage(message, { modal: true }, DialogResponses.yes); + if (result !== DialogResponses.yes) { + return; } } - } - - private insertLaunchConfig(existingConfigs: DebugConfiguration[] | undefined, newConfig: DebugConfiguration): DebugConfiguration[] { - // tslint:disable-next-line: strict-boolean-expressions - existingConfigs = existingConfigs || []; - // Remove configs that match the one we're about to add - existingConfigs = existingConfigs.filter((l1) => !isDebugConfigEqual(l1, newConfig)); - existingConfigs.push(newConfig); - return existingConfigs; - } - private async writeSettingsJson( - context: IProjectWizardContext, - vscodePath: string, - language: string, - version: FuncVersion - ): Promise { - const settings: ISettingToAdd[] = this.settings.concat( - { key: projectLanguageSetting, value: language }, - { key: funcVersionSetting, value: version }, - // We want the terminal to be open after F5, not the debug console (Since http triggers are printed in the terminal) - { prefix: 'debug', key: 'internalConsoleOptions', value: 'neverOpen' } - ); - - if (this.preDeployTask) { - settings.push({ key: preDeployTaskSetting, value: this.preDeployTask }); - } - - if (this.getWorkspaceSettings) { - const settingsToAdd = this.getWorkspaceSettings(); - settings.push(...settingsToAdd); - } - - if (context.workspaceFolder) { - // Use VS Code api to update config if folder is open - for (const setting of settings) { - await updateWorkspaceSetting(setting.key, setting.value, context.workspacePath, setting.prefix); - } - } else { - // otherwise manually edit json - const settingsJsonPath: string = path.join(vscodePath, settingsFileName); - await confirmEditJsonFile(context, settingsJsonPath, (data: Record): Record => { - for (const setting of settings) { - const key = `${setting.prefix || ext.prefix}.${setting.key}`; - data[key] = setting.value; - } - return data; - }); - } - } - - private async writeExtensionsJson(context: IActionContext, vscodePath: string, language: ProjectLanguage): Promise { - const extensionsJsonPath: string = path.join(vscodePath, extensionsFileName); - await confirmEditJsonFile(context, extensionsJsonPath, (data: IExtensionsJson): Record => { - const recommendations: string[] = [logicAppsStandardExtensionId]; - if (this.getRecommendedExtensions) { - recommendations.push(...this.getRecommendedExtensions(language)); - } - - if (data.recommendations) { - recommendations.push(...data.recommendations); - } + const tasksContent = generateTasksJson(projectConfig); + const launchContent = generateLaunchJson(projectConfig); + const settingsContent = generateSettingsJson(projectConfig); + const extensionsContent = generateExtensionsJson(); - // de-dupe array - data.recommendations = recommendations.filter((rec: string, index: number) => recommendations.indexOf(rec) === index); - return data; - }); + await writeFormattedJson(tasksJsonPath, tasksContent); + await writeFormattedJson(launchJsonPath, launchContent); + await writeFormattedJson(settingsJsonPath, settingsContent); + await writeFormattedJson(extensionsJsonPath, extensionsContent); } } diff --git a/apps/vs-code-designer/src/app/commands/initProjectForVSCode/initScriptProjectStep.ts b/apps/vs-code-designer/src/app/commands/initProjectForVSCode/initScriptProjectStep.ts deleted file mode 100644 index 75e09da84c3..00000000000 --- a/apps/vs-code-designer/src/app/commands/initProjectForVSCode/initScriptProjectStep.ts +++ /dev/null @@ -1,69 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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, funcDependencyName, funcWatchProblemMatcher, hostStartCommand } from '../../../constants'; -import { binariesExistSync } from '../../utils/binaries'; -import { getFuncHostTaskEnv } from '../../utils/codeless/funcHostTaskEnv'; -import { getLocalFuncCoreToolsVersion } from '../../utils/funcCoreTools/funcVersion'; -import { InitProjectStepBase } from './initProjectStepBase'; -import type { IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps'; -import { FuncVersion } from '@microsoft/vscode-extension-logic-apps'; -import * as fse from 'fs-extra'; -import * as path from 'path'; -import * as semver from 'semver'; -import type { TaskDefinition } from 'vscode'; - -/** - * Base class for all projects based on a simple script (i.e. JavaScript, C# Script, Bash, etc.) that don't require compilation - */ -export class InitScriptProjectStep extends InitProjectStepBase { - protected useFuncExtensionsInstall = false; - - protected async executeCore(context: IProjectWizardContext): Promise { - try { - const extensionsCsprojPath: string = path.join(context.projectPath, 'extensions.csproj'); - - if (await fse.pathExists(extensionsCsprojPath)) { - this.useFuncExtensionsInstall = true; - context.telemetry.properties.hasExtensionsCsproj = 'true'; - } else if (context.version === FuncVersion.v2) { - // no need to check v1 or v3+ - const currentVersion: string | null = await getLocalFuncCoreToolsVersion(); - // Starting after this version, projects can use extension bundle instead of running "func extensions install" - this.useFuncExtensionsInstall = !!currentVersion && semver.lte(currentVersion, '2.5.553'); - } - } catch { - // use default of false - } - - if (this.useFuncExtensionsInstall) { - // "func extensions install" task creates C# build artifacts that should be hidden - // See issue: https://github.com/Microsoft/vscode-azurefunctions/pull/699 - this.settings.push({ prefix: 'files', key: 'exclude', value: { obj: true, bin: true } }); - - if (!this.preDeployTask) { - this.preDeployTask = extInstallTaskName; - } - } - - await this.setDeploySubpath(context, '.'); - } - - protected getTasks(): TaskDefinition[] { - const funcBinariesExist = binariesExistSync(funcDependencyName); - const binariesOptions = funcBinariesExist ? getFuncHostTaskEnv() : {}; - return [ - { - 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/utils/__test__/debug.test.ts b/apps/vs-code-designer/src/app/utils/__test__/debug.test.ts index f6ac0a4dbda..a1557a5120a 100644 --- a/apps/vs-code-designer/src/app/utils/__test__/debug.test.ts +++ b/apps/vs-code-designer/src/app/utils/__test__/debug.test.ts @@ -1,23 +1,8 @@ -import { getCustomCodeRuntime, getDebugConfiguration, usesPublishFolderProperty } from '../debug'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { extensionCommand } from '../../../constants'; -import { FuncVersion, ProjectType, TargetFramework } from '@microsoft/vscode-extension-logic-apps'; +import { usesPublishFolderProperty } from '../debug'; +import { describe, it, expect } from 'vitest'; +import { ProjectType, TargetFramework } from '@microsoft/vscode-extension-logic-apps'; describe('debug', () => { - describe('getCustomCodeRuntime', () => { - it('should return coreclr for .NET 8', () => { - expect(getCustomCodeRuntime(TargetFramework.Net8)).toBe('coreclr'); - }); - - it('should return coreclr for .NET 10', () => { - expect(getCustomCodeRuntime(TargetFramework.Net10)).toBe('coreclr'); - }); - - it('should return clr for .NET Framework', () => { - expect(getCustomCodeRuntime(TargetFramework.NetFx)).toBe('clr'); - }); - }); - describe('usesPublishFolderProperty', () => { it('should return true for custom code with .NET 8', () => { expect(usesPublishFolderProperty(ProjectType.customCode, TargetFramework.Net8)).toBe(true); @@ -39,188 +24,4 @@ describe('debug', () => { expect(usesPublishFolderProperty(ProjectType.logicApp, TargetFramework.Net8)).toBe(false); }); }); - - describe('getDebugConfiguration', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('with custom code target framework (default isCodeless=true)', () => { - it('should return launch configuration for .NET 8 custom code with v4 function runtime', () => { - const result = getDebugConfiguration(FuncVersion.v4, 'TestLogicApp', TargetFramework.Net8); - expect(result).toEqual({ - name: 'Run/Debug logic app with local function TestLogicApp', - type: 'logicapp', - request: 'launch', - funcRuntime: 'coreclr', - customCodeRuntime: 'coreclr', - isCodeless: true, - }); - }); - - it('should return launch configuration for .NET 10 custom code with v4 function runtime', () => { - const result = getDebugConfiguration(FuncVersion.v4, 'TestLogicApp', TargetFramework.Net10); - - expect(result).toEqual({ - name: 'Run/Debug logic app with local function TestLogicApp', - type: 'logicapp', - request: 'launch', - funcRuntime: 'coreclr', - customCodeRuntime: 'coreclr', - isCodeless: true, - }); - }); - - it('should return launch configuration for .NET Framework custom code with v1 function runtime', () => { - const result = getDebugConfiguration(FuncVersion.v1, 'TestLogicApp', TargetFramework.NetFx); - - expect(result).toEqual({ - name: 'Run/Debug logic app with local function TestLogicApp', - type: 'logicapp', - request: 'launch', - funcRuntime: 'clr', - customCodeRuntime: 'clr', - isCodeless: true, - }); - }); - - it('should return launch configuration for .NET Framework custom code with v3 function runtime', () => { - const result = getDebugConfiguration(FuncVersion.v3, 'TestLogicApp', TargetFramework.NetFx); - - expect(result).toEqual({ - name: 'Run/Debug logic app with local function TestLogicApp', - type: 'logicapp', - request: 'launch', - funcRuntime: 'coreclr', - customCodeRuntime: 'clr', - isCodeless: true, - }); - }); - - it('should return launch configuration for .NET 8 custom code with v2 function runtime', () => { - const result = getDebugConfiguration(FuncVersion.v2, 'MyApp', TargetFramework.Net8); - - expect(result).toEqual({ - name: 'Run/Debug logic app with local function MyApp', - type: 'logicapp', - request: 'launch', - funcRuntime: 'coreclr', - customCodeRuntime: 'coreclr', - isCodeless: true, - }); - }); - }); - - describe('with codeful project (isCodeless=false)', () => { - it('should return launch configuration without customCodeRuntime for .NET 8 codeful with v4 runtime', () => { - const result = getDebugConfiguration(FuncVersion.v4, 'CodefulApp', TargetFramework.Net8, false); - - expect(result).toEqual({ - name: 'Run/Debug logic app CodefulApp', - type: 'logicapp', - request: 'launch', - funcRuntime: 'coreclr', - isCodeless: false, - }); - expect(result).not.toHaveProperty('customCodeRuntime'); - }); - - it('should return launch configuration without customCodeRuntime for .NET Framework codeful with v1 runtime', () => { - const result = getDebugConfiguration(FuncVersion.v1, 'CodefulApp', TargetFramework.NetFx, false); - - expect(result).toEqual({ - name: 'Run/Debug logic app CodefulApp', - type: 'logicapp', - request: 'launch', - funcRuntime: 'clr', - isCodeless: false, - }); - expect(result).not.toHaveProperty('customCodeRuntime'); - }); - }); - - describe('without custom code target framework', () => { - it('should return attach configuration for v1 function runtime', () => { - const result = getDebugConfiguration(FuncVersion.v1, 'TestLogicApp'); - - expect(result).toEqual({ - name: 'Run/Debug logic app TestLogicApp', - type: 'clr', - request: 'attach', - processId: `\${command:${extensionCommand.pickProcess}}`, - }); - }); - - it('should return attach configuration for v2 function runtime', () => { - const result = getDebugConfiguration(FuncVersion.v2, 'TestLogicApp'); - - expect(result).toEqual({ - name: 'Run/Debug logic app TestLogicApp', - type: 'coreclr', - request: 'attach', - processId: `\${command:${extensionCommand.pickProcess}}`, - }); - }); - - it('should return attach configuration for v3 function runtime', () => { - const result = getDebugConfiguration(FuncVersion.v3, 'TestLogicApp'); - - expect(result).toEqual({ - name: 'Run/Debug logic app TestLogicApp', - type: 'coreclr', - request: 'attach', - processId: `\${command:${extensionCommand.pickProcess}}`, - }); - }); - - it('should return attach configuration for v4 function runtime', () => { - const result = getDebugConfiguration(FuncVersion.v4, 'MyLogicApp'); - - expect(result).toEqual({ - name: 'Run/Debug logic app MyLogicApp', - type: 'coreclr', - request: 'attach', - processId: `\${command:${extensionCommand.pickProcess}}`, - }); - }); - }); - - describe('edge cases', () => { - it('should handle empty logic app name with custom code', () => { - const result = getDebugConfiguration(FuncVersion.v4, '', TargetFramework.Net8); - - expect(result).toEqual({ - name: 'Run/Debug logic app with local function ', - type: 'logicapp', - request: 'launch', - funcRuntime: 'coreclr', - customCodeRuntime: 'coreclr', - isCodeless: true, - }); - }); - - it('should handle empty logic app name without custom code', () => { - const result = getDebugConfiguration(FuncVersion.v3, ''); - - expect(result).toEqual({ - name: 'Run/Debug logic app ', - type: 'coreclr', - request: 'attach', - processId: `\${command:${extensionCommand.pickProcess}}`, - }); - }); - - it('should handle special characters in logic app name', () => { - const logicAppName = 'Test-App_With.Special@Characters'; - const result = getDebugConfiguration(FuncVersion.v4, logicAppName); - - expect(result).toEqual({ - name: `Run/Debug logic app ${logicAppName}`, - type: 'coreclr', - request: 'attach', - processId: `\${command:${extensionCommand.pickProcess}}`, - }); - }); - }); - }); }); diff --git a/apps/vs-code-designer/src/app/utils/__test__/extensionAssets.test.ts b/apps/vs-code-designer/src/app/utils/__test__/extensionAssets.test.ts index 1e4f7b59a0c..503488962e5 100644 --- a/apps/vs-code-designer/src/app/utils/__test__/extensionAssets.test.ts +++ b/apps/vs-code-designer/src/app/utils/__test__/extensionAssets.test.ts @@ -50,12 +50,4 @@ describe('getExtensionAssetPath', () => { expect(assetPath.endsWith(path.join('vs-code-designer', 'assets', 'ContainerTemplates', 'devcontainer.json'))).toBe(true); }); - - it('returns the first fallback candidate when neither assets layout exists', () => { - vi.mocked(existsSync).mockReturnValue(false); - - const assetPath = getExtensionAssetPath('WorkspaceTemplates', 'ExtensionsJsonFile'); - - expect(assetPath.endsWith(path.join('src', 'assets', 'WorkspaceTemplates', 'ExtensionsJsonFile'))).toBe(true); - }); }); diff --git a/apps/vs-code-designer/src/app/utils/codeless/__test__/validateProjectArtifacts.test.ts b/apps/vs-code-designer/src/app/utils/codeless/__test__/validateProjectArtifacts.test.ts index ca76d341b55..4a2c961c66f 100644 --- a/apps/vs-code-designer/src/app/utils/codeless/__test__/validateProjectArtifacts.test.ts +++ b/apps/vs-code-designer/src/app/utils/codeless/__test__/validateProjectArtifacts.test.ts @@ -27,10 +27,9 @@ import { } from '../../../../constants'; import * as localSettings from '../../appSettings/localSettings'; import { writeFormattedJson } from '../../fs'; -import { hasCodefulSdkReference } from '../../codeful'; -import { isCustomCodeFunctionsProjectInRoot } from '../../customCodeUtils'; +import { hasCodefulSdkReference, hasCodefulWorkflowSetting } from '../../codeful'; +import { isCustomCodeFunctionsProjectInRoot, tryGetLogicAppCustomCodeFunctionsProjects } from '../../customCodeUtils'; import { - detectLogicAppProjectType, extractAppSettingReferences, getReferencedAppSettings, regenerateLocalSettings, @@ -39,6 +38,7 @@ import { validateDesignTimeDirectory, regenerateDesignTimeDirectory, } from '../validateProjectArtifacts'; +import { detectProjectType } from '../../project'; vi.mock('fs-extra', () => ({ pathExists: vi.fn(), @@ -61,10 +61,12 @@ vi.mock('../../fs', () => ({ vi.mock('../../codeful', () => ({ hasCodefulSdkReference: vi.fn(() => Promise.resolve(false)), + hasCodefulWorkflowSetting: vi.fn(() => Promise.resolve(false)), })); vi.mock('../../customCodeUtils', () => ({ isCustomCodeFunctionsProjectInRoot: vi.fn(() => Promise.resolve(false)), + tryGetLogicAppCustomCodeFunctionsProjects: vi.fn(() => Promise.resolve(undefined)), })); const projectPath = '/workspace/LogicApp'; @@ -82,6 +84,8 @@ const mockedGetLocalSettingsJson = localSettings.getLocalSettingsJson as unknown const mockedWriteFormattedJson = writeFormattedJson as unknown as ReturnType; const mockedIsCodeful = hasCodefulSdkReference as unknown as ReturnType; const mockedIsCustomCodeInRoot = isCustomCodeFunctionsProjectInRoot as unknown as ReturnType; +const mockedHasCodefulWorkflowSetting = hasCodefulWorkflowSetting as unknown as ReturnType; +const mockedTryGetCustomCodeProjects = tryGetLogicAppCustomCodeFunctionsProjects as unknown as ReturnType; /** Returns the object written via writeFormattedJson for the file whose path ends with fileName. */ function writtenContentFor(fileName: string): unknown { @@ -236,9 +240,9 @@ describe('validateProjectArtifacts', () => { // Behavior by logic app type: regeneration builds the root local.settings.json from the same shared // source of truth as fresh project creation (getLocalSettingsSchema). The project type is inferred from - // the project files (detectLogicAppProjectType): codeful via hasCodefulSdkReference, and customCode / - // rulesEngine via a sibling custom-code functions (.csproj) project in the workspace root. As a - // result every type regenerates the same content a freshly created project of that type would. + // the project files (detectProjectType): codeful via hasCodefulWorkflowSetting/hasCodefulSdkReference, + // and customCode via tryGetLogicAppCustomCodeFunctionsProjects. As a result every type regenerates + // the same content a freshly created project of that type would. describe('regenerateLocalSettings — behavior by logic app type', () => { const codelessBaseline = { [appKindSetting]: logicAppKind, @@ -268,10 +272,11 @@ describe('validateProjectArtifacts', () => { }); it('customCode: regenerates the codeless baseline plus AzureWebJobsFeatureFlags (sibling custom-code project detected)', async () => { - // A sibling custom-code functions project in the workspace root identifies this as a customCode + // A sibling custom-code functions project identifies this as a customCode // logic app, which gets the EnableMultiLanguageWorker flag just like fresh creation. mockedIsCodeful.mockResolvedValue(false); - mockedIsCustomCodeInRoot.mockResolvedValue(true); + mockedHasCodefulWorkflowSetting.mockResolvedValue(false); + mockedTryGetCustomCodeProjects.mockResolvedValue(['some/custom-code-path']); const changed = await regenerateLocalSettings(context, projectPath); @@ -288,7 +293,8 @@ describe('validateProjectArtifacts', () => { // rulesEngine cannot be told apart from customCode at regeneration time, but both produce the same // root local.settings.json, so detecting the sibling custom-code project is sufficient. mockedIsCodeful.mockResolvedValue(false); - mockedIsCustomCodeInRoot.mockResolvedValue(true); + mockedHasCodefulWorkflowSetting.mockResolvedValue(false); + mockedTryGetCustomCodeProjects.mockResolvedValue(['some/rules-engine-path']); const changed = await regenerateLocalSettings(context, projectPath); @@ -316,43 +322,44 @@ describe('validateProjectArtifacts', () => { }); }); - describe('detectLogicAppProjectType', () => { - it('returns codeful when the project itself is a codeful project', async () => { - mockedIsCodeful.mockResolvedValue(true); - mockedIsCustomCodeInRoot.mockResolvedValue(true); + describe('detectProjectType', () => { + it('returns codeful when the project has codeful workflow setting', async () => { + mockedHasCodefulWorkflowSetting.mockResolvedValue(true); + mockedTryGetCustomCodeProjects.mockResolvedValue(['some/path']); // Codeful takes precedence even when a sibling custom-code project is also present. - expect(await detectLogicAppProjectType(projectPath)).toBe(ProjectType.codeful); + expect(await detectProjectType(projectPath)).toBe(ProjectType.codeful); }); - it('returns customCode when a sibling custom-code functions project exists in the workspace root', async () => { - mockedIsCodeful.mockResolvedValue(false); - mockedIsCustomCodeInRoot.mockResolvedValue(true); + it('returns codeful when the project has codeful SDK reference', async () => { + mockedHasCodefulWorkflowSetting.mockResolvedValue(false); + mockedIsCodeful.mockResolvedValue(true); - expect(await detectLogicAppProjectType(projectPath)).toBe(ProjectType.customCode); + expect(await detectProjectType(projectPath)).toBe(ProjectType.codeful); }); - it('returns logicApp when the project is neither codeful nor has a sibling custom-code project', async () => { + it('returns customCode when a sibling custom-code functions project exists', async () => { + mockedHasCodefulWorkflowSetting.mockResolvedValue(false); mockedIsCodeful.mockResolvedValue(false); - mockedIsCustomCodeInRoot.mockResolvedValue(false); + mockedTryGetCustomCodeProjects.mockResolvedValue(['some/custom-code-path']); - expect(await detectLogicAppProjectType(projectPath)).toBe(ProjectType.logicApp); + expect(await detectProjectType(projectPath)).toBe(ProjectType.customCode); }); - it('treats undefined detection results as not-detected (logicApp)', async () => { - mockedIsCodeful.mockResolvedValue(undefined); - mockedIsCustomCodeInRoot.mockResolvedValue(undefined); + it('returns logicApp when the project is neither codeful nor has custom-code siblings', async () => { + mockedHasCodefulWorkflowSetting.mockResolvedValue(false); + mockedIsCodeful.mockResolvedValue(false); + mockedTryGetCustomCodeProjects.mockResolvedValue(undefined); - expect(await detectLogicAppProjectType(projectPath)).toBe(ProjectType.logicApp); + expect(await detectProjectType(projectPath)).toBe(ProjectType.logicApp); }); - it('inspects the workspace root (the parent of the logic app folder) for custom-code siblings', async () => { + it('returns logicApp when custom code projects list is empty', async () => { + mockedHasCodefulWorkflowSetting.mockResolvedValue(false); mockedIsCodeful.mockResolvedValue(false); - mockedIsCustomCodeInRoot.mockResolvedValue(false); - - await detectLogicAppProjectType(projectPath); + mockedTryGetCustomCodeProjects.mockResolvedValue([]); - expect(mockedIsCustomCodeInRoot).toHaveBeenCalledWith(path.dirname(projectPath)); + expect(await detectProjectType(projectPath)).toBe(ProjectType.logicApp); }); }); diff --git a/apps/vs-code-designer/src/app/utils/codeless/validateProjectArtifacts.ts b/apps/vs-code-designer/src/app/utils/codeless/validateProjectArtifacts.ts index 1c76d29c7cb..0f71951ff60 100644 --- a/apps/vs-code-designer/src/app/utils/codeless/validateProjectArtifacts.ts +++ b/apps/vs-code-designer/src/app/utils/codeless/validateProjectArtifacts.ts @@ -23,14 +23,13 @@ import { ext } from '../../../extensionVariables'; import { addOrUpdateLocalAppSettings, getLocalSettingsJson, getLocalSettingsSchema } from '../appSettings/localSettings'; import { writeFormattedJson } from '../fs'; import { parseJson } from '../parseJson'; -import { hasCodefulSdkReference } from '../codeful'; -import { isCustomCodeFunctionsProjectInRoot } from '../customCodeUtils'; -import { ProjectType, WorkerRuntime } from '@microsoft/vscode-extension-logic-apps'; +import { WorkerRuntime } from '@microsoft/vscode-extension-logic-apps'; import type { IHostJsonV2, ILocalSettingsJson } from '@microsoft/vscode-extension-logic-apps'; import type { IActionContext } from '@microsoft/vscode-azext-utils'; import * as fse from 'fs-extra'; import * as path from 'path'; import { Uri, workspace } from 'vscode'; +import { detectProjectType } from '../project'; /** * Matches app setting references such as `@appsetting('MY_SETTING')` and the interpolated @@ -115,33 +114,6 @@ export async function getReferencedAppSettings(projectPath: string): Promise} The inferred project type. - */ -export async function detectLogicAppProjectType(projectPath: string): Promise { - if ((await hasCodefulSdkReference(projectPath)) ?? false) { - return ProjectType.codeful; - } - - const hasCustomCodeSibling = (await isCustomCodeFunctionsProjectInRoot(path.dirname(projectPath))) ?? false; - if (hasCustomCodeSibling) { - return ProjectType.customCode; - } - - return ProjectType.logicApp; -} - /** * Ensures the project-level local.settings.json exists and contains every app setting the project * requires. This is needed when source control is enabled: local.settings.json is git-ignored, so a @@ -170,7 +142,7 @@ export async function regenerateLocalSettings(context: IActionContext, projectPa // Build the baseline from the same source of truth as fresh project creation so a regenerated // local.settings.json matches what a newly created project of this type would produce. The project // type is inferred from the project files because a source-controlled clone has no explicit marker. - const logicAppType = await detectLogicAppProjectType(projectPath); + const logicAppType = await detectProjectType(projectPath); const baselineValues = getLocalSettingsSchema(false, projectPath, logicAppType).Values ?? {}; const referencedSettings = await getReferencedAppSettings(projectPath); @@ -406,7 +378,7 @@ export async function regenerateDesignTimeDirectory(context: IActionContext, pro } if (!validation.settingsFileValid) { - const logicAppType = await detectLogicAppProjectType(projectPath); + const logicAppType = await detectProjectType(projectPath); const settingsFileContent = getLocalSettingsSchema(true, projectPath, logicAppType); await writeFormattedJson(path.join(designTimeDirectory.fsPath, localSettingsFileName), settingsFileContent); await addOrUpdateLocalAppSettings( diff --git a/apps/vs-code-designer/src/app/utils/customCodeUtils.ts b/apps/vs-code-designer/src/app/utils/customCodeUtils.ts index d2d480e1124..f04ac753561 100644 --- a/apps/vs-code-designer/src/app/utils/customCodeUtils.ts +++ b/apps/vs-code-designer/src/app/utils/customCodeUtils.ts @@ -89,6 +89,21 @@ export async function isCustomCodeFunctionsProject(folderPath: string): Promise< return !isNullOrUndefined(getCustomCodeTargetFramework(csprojContent)); } +/** + * Detects the target framework of a custom code functions project for the given logic app project. + * @param {string} projectPath - The path to the logic app project. + * @returns {Promise} Returns the target framework if found, otherwise undefined. + */ +export async function detectCustomCodeTargetFramework(projectPath: string): Promise { + const customCodeProjects = await tryGetLogicAppCustomCodeFunctionsProjects(projectPath); + if (customCodeProjects && customCodeProjects.length > 0) { + const metadata = await getCustomCodeFunctionsProjectMetadata(customCodeProjects[0]); + return metadata?.targetFramework; + } + + return undefined; +} + /** * Gets the metadata of a custom code functions project. * @param {string} folderPath - The folder path of the custom code functions project. diff --git a/apps/vs-code-designer/src/app/utils/debug.ts b/apps/vs-code-designer/src/app/utils/debug.ts index 0c5b042bdce..d081e902643 100644 --- a/apps/vs-code-designer/src/app/utils/debug.ts +++ b/apps/vs-code-designer/src/app/utils/debug.ts @@ -2,8 +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 { FuncVersion, ProjectType, TargetFramework } from '@microsoft/vscode-extension-logic-apps'; -import type { DebugConfiguration, DebugConfigurationProvider } from 'vscode'; +import { ProjectType, TargetFramework } from '@microsoft/vscode-extension-logic-apps'; +import type { DebugConfigurationProvider } from 'vscode'; import { debugSymbolDll, extensionBundleId, extensionCommand } from '../../constants'; import * as vscode from 'vscode'; import * as path from 'path'; @@ -82,10 +82,6 @@ export async function getDebugSymbolDll(): Promise { return path.join(bundleFolder, bundleVersionNumber, 'bin', debugSymbolDll); } -export function getCustomCodeRuntime(targetFramework: TargetFramework): 'coreclr' | 'clr' { - return targetFramework === TargetFramework.NetFx ? 'clr' : 'coreclr'; -} - /** * Determines whether the given project type and target framework use the modern * LogicAppFolderToPublish csproj property (as opposed to the legacy LogicAppFolder). @@ -94,48 +90,3 @@ export function getCustomCodeRuntime(targetFramework: TargetFramework): 'coreclr export function usesPublishFolderProperty(projectType: ProjectType, targetFramework: TargetFramework): boolean { return projectType === ProjectType.customCode && targetFramework !== TargetFramework.NetFx; } - -/** - * Generates a debug configuration for a Logic App based on the function version and optional custom code framework. - * @param version - The Azure Functions runtime version (v1, v2, v3, or v4) - * @param logicAppName - The name of the Logic App to debug - * @param customCodeTargetFramework - Optional target framework for custom code (.NET 8 or .NET Framework). - * When provided, returns a launch configuration with `type: 'logicapp'`. - * @param isCodeless - Whether the project uses codeless (JSON) workflow definitions. Defaults to `true`. - * - `true` (custom code): includes `customCodeRuntime` for attaching a second debugger to the .NET host. - * - `false` (codeful): omits `customCodeRuntime` since the workflow IS the compiled code. - * - * @returns A DebugConfiguration object with either: - * - Launch configuration for Logic Apps with custom code (isCodeless=true), including both function and custom code runtime settings - * - Launch configuration for codeful Logic Apps (isCodeless=false), with function runtime only - * - Attach configuration for standard Logic Apps (no customCodeTargetFramework), allowing process selection for debugging - */ -export const getDebugConfiguration = ( - version: FuncVersion, - logicAppName: string, - customCodeTargetFramework?: TargetFramework, - isCodeless = true -): DebugConfiguration => { - if (customCodeTargetFramework) { - const config: DebugConfiguration = { - name: isCodeless ? `Run/Debug logic app with local function ${logicAppName}` : `Run/Debug logic app ${logicAppName}`, - type: 'logicapp', - request: 'launch', - funcRuntime: version === FuncVersion.v1 ? 'clr' : 'coreclr', - isCodeless, - }; - - if (isCodeless) { - config.customCodeRuntime = getCustomCodeRuntime(customCodeTargetFramework); - } - - return config; - } - - return { - name: `Run/Debug logic app ${logicAppName}`, - type: version === FuncVersion.v1 ? 'clr' : 'coreclr', - request: 'attach', - processId: `\${command:${extensionCommand.pickProcess}}`, - }; -}; 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 8c7122cb659..c257a4765ec 100644 --- a/apps/vs-code-designer/src/app/utils/dotnet/dotnet.ts +++ b/apps/vs-code-designer/src/app/utils/dotnet/dotnet.ts @@ -18,7 +18,7 @@ import { getGlobalSetting, updateGlobalSetting, updateWorkspaceSetting } from '. import { findFiles, getWorkspaceLogicAppFolders } from '../workspace'; import type { IActionContext } from '@microsoft/vscode-azext-utils'; import { AzExtFsExtra } from '@microsoft/vscode-azext-utils'; -import type { IWorkerRuntime } from '@microsoft/vscode-extension-logic-apps'; +import { type IWorkerRuntime, TargetFramework } from '@microsoft/vscode-extension-logic-apps'; import { FuncVersion, Platform, ProjectLanguage } from '@microsoft/vscode-extension-logic-apps'; import * as fs from 'fs'; import * as path from 'path'; @@ -92,24 +92,60 @@ function getProjectTemplateKey(targetFramework: string, isIsolated: boolean): st * Gets property in project file. * @param {ProjectFile} projFile - File. * @param {string} property - Property key name. - * @returns {Promise} Property value. + * @returns {Promise} Property value. */ -async function getPropertyInProjFile(projFile: ProjectFile, property: string): Promise { +async function tryGetPropertyInProjFile(projFile: ProjectFile, property: string): Promise { const regExp = new RegExp(`<${property}>(.*)<\\/${property}>`); const matches: RegExpMatchArray | null = (await projFile.getContents()).match(regExp); if (!matches) { - throw new Error(localize('failedToFindProp', 'Failed to find "{0}" in project file "{1}".', property, projFile.name)); + return undefined; } return matches[1]; } +/** + * Attempts to read the target framework from the first .csproj/.fsproj in the folder. + * @param {string} projectPath - The logic app project path. + * @returns {Promise} The target framework or undefined if not found. + */ +export async function tryGetTargetFramework(projectPath: string): Promise { + const files = fs.readdirSync(projectPath); + const projFileName = files.find((f) => (f.endsWith('.csproj') || f.endsWith('.fsproj')) && f.toLowerCase() !== 'extensions.csproj'); + if (projFileName) { + return await getTargetFramework(new ProjectFile(projFileName, projectPath)); + } + return undefined; +} + /** * Gets target framework property in project file. - * @param {ProjectFile} projFile - File. - * @returns {Promise} Property value. + * @param {ProjectFile} projFile - The .csproj or .fsproj file. + * @returns {Promise} The target framework. */ -export async function getTargetFramework(projFile: ProjectFile): Promise { - return await getPropertyInProjFile(projFile, 'TargetFramework'); +export async function getTargetFramework(projFile: ProjectFile): Promise { + const targetFramework = await tryGetPropertyInProjFile(projFile, 'TargetFramework'); + if (!targetFramework) { + throw new Error(localize('targetFrameworkNotFound', 'Failed to parse TargetFramework in project file "{1}".', projFile.name)); + } + return targetFramework as TargetFramework; +} + +/** + * Gets the .NET runtime from target framework. + * @param {TargetFramework} targetFramework - The target framework. + * @returns {'coreclr' | 'clr'} The .NET runtime. + */ +export function getDotnetRuntimeFromFramework(targetFramework: TargetFramework): 'coreclr' | 'clr' { + return targetFramework === TargetFramework.NetFx ? 'clr' : 'coreclr'; +} + +/** + * Gets the .NET runtime from Func version. + * @param {FuncVersion} funcVersion - The Func version. + * @returns {'coreclr' | 'clr'} The .NET runtime. + */ +export function getDotnetRuntimeFromFunc(funcVersion: FuncVersion): 'coreclr' | 'clr' { + return funcVersion === FuncVersion.v1 ? 'clr' : 'coreclr'; } /** @@ -174,11 +210,7 @@ export function getDotnetDebugSubpath(targetFramework: string): string { * @returns {Promise} Property value. */ export async function tryGetFuncVersion(projFile: ProjectFile): Promise { - try { - return await getPropertyInProjFile(projFile, 'AzureFunctionsVersion'); - } catch { - return undefined; - } + return await tryGetPropertyInProjFile(projFile, 'AzureFunctionsVersion'); } export function getTemplateKeyFromFeedEntry(runtimeInfo: IWorkerRuntime): string { diff --git a/apps/vs-code-designer/src/app/utils/project.ts b/apps/vs-code-designer/src/app/utils/project.ts new file mode 100644 index 00000000000..8559ea01daf --- /dev/null +++ b/apps/vs-code-designer/src/app/utils/project.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as fse from 'fs-extra'; +import { hasCodefulSdkReference, hasCodefulWorkflowSetting } from './codeful'; +import { tryGetLogicAppCustomCodeFunctionsProjects } from './customCodeUtils'; +import { ProjectType, ProjectPackageType } from '@microsoft/vscode-extension-logic-apps'; + +/** + * Detects the Logic App project type at the given path by inspecting + * project files, settings, and sibling folders. + */ +export async function detectProjectType(projectPath: string): Promise { + if ((await hasCodefulWorkflowSetting(projectPath)) || (await hasCodefulSdkReference(projectPath))) { + return ProjectType.codeful; + } + + const customCodeProjects = await tryGetLogicAppCustomCodeFunctionsProjects(projectPath); + if (customCodeProjects && customCodeProjects.length > 0) { + return ProjectType.customCode; + } + + return ProjectType.logicApp; +} + +/** + * Detects the project package type (packaging model) at the given path. + */ +export async function detectProjectPackageType(projectPath: string): Promise { + if (await hasDotnetProjectFile(projectPath)) { + return ProjectPackageType.Nuget; + } + return ProjectPackageType.Bundle; +} + +/** + * Checks whether the folder contains a `.csproj` or `.fsproj` file. + */ +async function hasDotnetProjectFile(folderPath: string): Promise { + try { + const files = await fse.readdir(folderPath); + return files.some((f) => (f.endsWith('.csproj') || f.endsWith('.fsproj')) && f.toLowerCase() !== 'extensions.csproj'); + } catch { + return false; + } +} diff --git a/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/__test__/extensionsGenerator.test.ts b/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/__test__/extensionsGenerator.test.ts new file mode 100644 index 00000000000..b69cacf3557 --- /dev/null +++ b/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/__test__/extensionsGenerator.test.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { describe, it, expect } from 'vitest'; +import { generateExtensionsJson } from '../extensionsGenerator'; + +describe('generateExtensionsJson', () => { + it('should include all standard recommendations', () => { + const result = generateExtensionsJson(); + + expect(result.recommendations).toContain('ms-azuretools.vscode-azurelogicapps'); + expect(result.recommendations).toContain('ms-azuretools.vscode-azurefunctions'); + expect(result.recommendations).toContain('ms-dotnettools.csharp'); + expect(result.recommendations).toContain('ms-dotnettools.csdevkit'); + }); +}); \ No newline at end of file diff --git a/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/__test__/launchGenerator.test.ts b/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/__test__/launchGenerator.test.ts new file mode 100644 index 00000000000..7f0571699d3 --- /dev/null +++ b/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/__test__/launchGenerator.test.ts @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { describe, it, expect } from 'vitest'; +import { generateLaunchJson } from '../launchGenerator'; +import type { VSCodeProjectConfig } from '../types'; +import { FuncVersion, ProjectType, ProjectPackageType, TargetFramework } from '@microsoft/vscode-extension-logic-apps'; + +describe('generateLaunchJson', () => { + describe('codeless project', () => { + it('should generate attach configuration for codeless projects', () => { + const config: VSCodeProjectConfig = { + projectType: ProjectType.logicApp, + projectPackageType: ProjectPackageType.Bundle, + hasFuncBinaries: true, + logicAppName: 'MyApp', + funcVersion: FuncVersion.v4, + }; + const result = generateLaunchJson(config); + + expect(result.version).toBe('0.2.0'); + expect(result.configurations).toHaveLength(1); + expect(result.configurations[0].type).toBe('coreclr'); + expect(result.configurations[0].request).toBe('attach'); + expect(result.configurations[0].name).toContain('MyApp'); + }); + }); + + describe('codeful project', () => { + it('should generate logicapp launch configuration', () => { + const config: VSCodeProjectConfig = { + projectType: ProjectType.codeful, + projectPackageType: ProjectPackageType.Nuget, + hasFuncBinaries: true, + logicAppName: 'CodefulApp', + funcVersion: FuncVersion.v4, + }; + const result = generateLaunchJson(config); + + expect(result.configurations[0].type).toBe('logicapp'); + expect(result.configurations[0].request).toBe('launch'); + expect(result.configurations[0].funcRuntime).toBe('coreclr'); + expect(result.configurations[0].isCodeless).toBe(false); + }); + }); + + describe('custom code project', () => { + it('should generate logicapp launch configuration with customCodeRuntime', () => { + const config: VSCodeProjectConfig = { + projectType: ProjectType.customCode, + projectPackageType: ProjectPackageType.Bundle, + hasFuncBinaries: true, + logicAppName: 'CustomCodeApp', + funcVersion: FuncVersion.v4, + customCodeTargetFramework: TargetFramework.Net8, + }; + const result = generateLaunchJson(config); + + expect(result.configurations[0].type).toBe('logicapp'); + expect(result.configurations[0].request).toBe('launch'); + expect(result.configurations[0].customCodeRuntime).toBe('coreclr'); + expect(result.configurations[0].isCodeless).toBe(true); + }); + + it('should use clr runtime for NetFx custom code', () => { + const config: VSCodeProjectConfig = { + projectType: ProjectType.customCode, + projectPackageType: ProjectPackageType.Bundle, + hasFuncBinaries: true, + logicAppName: 'NetFxApp', + funcVersion: FuncVersion.v4, + customCodeTargetFramework: TargetFramework.NetFx, + }; + const result = generateLaunchJson(config); + + expect(result.configurations[0].customCodeRuntime).toBe('clr'); + }); + + it('should use coreclr runtime for Net10 custom code', () => { + const config: VSCodeProjectConfig = { + projectType: ProjectType.customCode, + projectPackageType: ProjectPackageType.Bundle, + hasFuncBinaries: true, + logicAppName: 'Net10App', + funcVersion: FuncVersion.v4, + customCodeTargetFramework: TargetFramework.Net10, + }; + const result = generateLaunchJson(config); + + expect(result.configurations[0].customCodeRuntime).toBe('coreclr'); + }); + }); + + describe('codeful project', () => { + it('should generate logicapp launch configuration', () => { + const config: VSCodeProjectConfig = { + projectType: ProjectType.codeful, + projectPackageType: ProjectPackageType.Nuget, + hasFuncBinaries: true, + logicAppName: 'CodefulApp', + funcVersion: FuncVersion.v4, + }; + const result = generateLaunchJson(config); + + expect(result.configurations[0].type).toBe('logicapp'); + expect(result.configurations[0].request).toBe('launch'); + expect(result.configurations[0].funcRuntime).toBe('coreclr'); + expect(result.configurations[0].isCodeless).toBe(false); + }); + + it('should not include customCodeRuntime for codeful projects', () => { + const config: VSCodeProjectConfig = { + projectType: ProjectType.codeful, + projectPackageType: ProjectPackageType.Nuget, + hasFuncBinaries: true, + logicAppName: 'CodefulApp', + funcVersion: FuncVersion.v4, + }; + const result = generateLaunchJson(config); + + expect(result.configurations[0]).not.toHaveProperty('customCodeRuntime'); + }); + }); + + describe('FuncVersion handling', () => { + it('should use clr type for v1 attach config', () => { + const config: VSCodeProjectConfig = { + projectType: ProjectType.logicApp, + projectPackageType: ProjectPackageType.Bundle, + hasFuncBinaries: true, + logicAppName: 'V1App', + funcVersion: FuncVersion.v1, + }; + const result = generateLaunchJson(config); + + expect(result.configurations[0].type).toBe('clr'); + }); + }); +}); diff --git a/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/__test__/settingsGenerator.test.ts b/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/__test__/settingsGenerator.test.ts new file mode 100644 index 00000000000..de2d18b242d --- /dev/null +++ b/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/__test__/settingsGenerator.test.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { describe, it, expect } from 'vitest'; +import { generateSettingsJson } from '../settingsGenerator'; +import type { VSCodeProjectConfig } from '../types'; +import { ProjectType, ProjectPackageType, TargetFramework } from '@microsoft/vscode-extension-logic-apps'; + +describe('generateSettingsJson', () => { + describe('codeless project', () => { + it('should generate base settings with deploySubpath "."', () => { + const config: VSCodeProjectConfig = { + projectType: ProjectType.logicApp, + projectPackageType: ProjectPackageType.Bundle, + hasFuncBinaries: true, + }; + const result = generateSettingsJson(config); + + expect(result).toHaveProperty('azureLogicAppsStandard.projectLanguage'); + expect(result).toHaveProperty('azureLogicAppsStandard.projectRuntime'); + expect(result).toHaveProperty('debug.internalConsoleOptions', 'neverOpen'); + expect(result).toHaveProperty('azureFunctions.suppressProject', true); + expect(result).toHaveProperty('azureLogicAppsStandard.deploySubpath', '.'); + }); + + it('should default to JavaScript language', () => { + const config: VSCodeProjectConfig = { + projectType: ProjectType.logicApp, + projectPackageType: ProjectPackageType.Bundle, + hasFuncBinaries: true, + }; + const result = generateSettingsJson(config); + + expect(result['azureLogicAppsStandard.projectLanguage']).toBe('JavaScript'); + }); + }); + + describe('codeful project', () => { + it('should add deploy/publish and OmniSharp settings', () => { + const config: VSCodeProjectConfig = { + projectType: ProjectType.codeful, + projectPackageType: ProjectPackageType.Nuget, + hasFuncBinaries: true, + targetFramework: TargetFramework.Net8, + }; + const result = generateSettingsJson(config); + + expect(result).toHaveProperty('azureFunctions.deploySubpath'); + expect(result).toHaveProperty('azureFunctions.preDeployTask', 'publish'); + expect(result).toHaveProperty('azureFunctions.projectSubpath'); + expect(result).toHaveProperty('omnisharp.enableMsBuildLoadProjectsOnDemand', false); + expect(result).toHaveProperty('omnisharp.disableMSBuildDiagnosticWarning', true); + }); + + it('should use CSharp language for codeful', () => { + const config: VSCodeProjectConfig = { + projectType: ProjectType.codeful, + projectPackageType: ProjectPackageType.Nuget, + hasFuncBinaries: true, + }; + const result = generateSettingsJson(config); + + expect(result['azureLogicAppsStandard.projectLanguage']).toBe('C#'); + }); + }); + + describe('nuget project', () => { + it('should add deploySubpath and preDeployTask without OmniSharp', () => { + const config: VSCodeProjectConfig = { + projectType: ProjectType.logicApp, + projectPackageType: ProjectPackageType.Nuget, + hasFuncBinaries: true, + targetFramework: TargetFramework.Net8, + }; + const result = generateSettingsJson(config); + + expect(result).toHaveProperty('azureLogicAppsStandard.deploySubpath'); + expect(result).toHaveProperty('azureFunctions.preDeployTask'); + expect(result).not.toHaveProperty('omnisharp.enableMsBuildLoadProjectsOnDemand'); + }); + }); +}); diff --git a/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/__test__/tasksGenerator.test.ts b/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/__test__/tasksGenerator.test.ts new file mode 100644 index 00000000000..35078991679 --- /dev/null +++ b/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/__test__/tasksGenerator.test.ts @@ -0,0 +1,222 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { describe, it, expect, vi } from 'vitest'; +import { generateTasksJson } from '../tasksGenerator'; +import type { VSCodeProjectConfig } from '../types'; +import { ProjectType, ProjectPackageType, TargetFramework } from '@microsoft/vscode-extension-logic-apps'; + +vi.mock('../../../codeless/funcHostTaskEnv', () => ({ + getFuncHostTaskEnv: (extras?: { cwd?: string }) => ({ + options: { ...(extras?.cwd ? { cwd: extras.cwd } : {}), env: { PATH: '${env:PATH}' } }, + windows: { options: { ...(extras?.cwd ? { cwd: extras.cwd } : {}), env: { PATH: 'win-path' } } }, + linux: { options: { ...(extras?.cwd ? { cwd: extras.cwd } : {}), env: { PATH: 'linux-path' } } }, + osx: { options: { ...(extras?.cwd ? { cwd: extras.cwd } : {}), env: { PATH: 'osx-path' } } }, + }), +})); + +describe('generateTasksJson', () => { + describe('codeless project', () => { + const baseConfig: VSCodeProjectConfig = { + projectType: ProjectType.logicApp, + projectPackageType: ProjectPackageType.Bundle, + hasFuncBinaries: true, + }; + + it('should generate tasks with generateDebugSymbols and func:host start', () => { + const result = generateTasksJson(baseConfig); + + expect(result.version).toBe('2.0.0'); + expect(result.tasks).toHaveLength(2); + expect(result.tasks[0].label).toBe('generateDebugSymbols'); + expect(result.tasks[1].label).toBe('func: host start'); + }); + + it('should include inputs with getDebugSymbolDll', () => { + const result = generateTasksJson(baseConfig); + + expect(result.inputs).toHaveLength(1); + expect(result.inputs[0].id).toBe('getDebugSymbolDll'); + expect(result.inputs[0].command).toBe('azureLogicAppsStandard.getDebugSymbolDll'); + }); + + it('should use shell type when binaries exist', () => { + const result = generateTasksJson(baseConfig); + const funcTask = result.tasks[1]; + + expect(funcTask.type).toBe('shell'); + expect(funcTask.command).toBe('${config:azureLogicAppsStandard.funcCoreToolsBinaryPath}'); + expect(funcTask.args).toEqual(['host', 'start']); + }); + + it('should use func type when binaries do not exist', () => { + const result = generateTasksJson({ ...baseConfig, hasFuncBinaries: false }); + const funcTask = result.tasks[1]; + + expect(funcTask.type).toBe('func'); + expect(funcTask.command).toBe('host start'); + expect(funcTask.args).toBeUndefined(); + }); + + it('should include platform-keyed env options when binaries exist', () => { + const result = generateTasksJson(baseConfig); + const funcTask = result.tasks[1]; + + expect(funcTask.options).toBeDefined(); + expect(funcTask.windows).toBeDefined(); + expect(funcTask.linux).toBeDefined(); + expect(funcTask.osx).toBeDefined(); + }); + + it('should not include env options when binaries do not exist', () => { + const result = generateTasksJson({ ...baseConfig, hasFuncBinaries: false }); + const funcTask = result.tasks[1]; + + expect(funcTask.options).toBeUndefined(); + expect(funcTask.windows).toBeUndefined(); + }); + + it('should suppress env options for devcontainer projects', () => { + const result = generateTasksJson({ ...baseConfig, isDevContainer: true }); + const funcTask = result.tasks[1]; + + expect(funcTask.options).toBeUndefined(); + expect(funcTask.windows).toBeUndefined(); + expect(funcTask.linux).toBeUndefined(); + expect(funcTask.osx).toBeUndefined(); + }); + + it('should include group on func:host start for codeless', () => { + const result = generateTasksJson(baseConfig); + const funcTask = result.tasks[1]; + + expect(funcTask.group).toEqual({ kind: 'build', isDefault: true }); + }); + + it('should not include dependsOn for codeless func:host start', () => { + const result = generateTasksJson(baseConfig); + const funcTask = result.tasks[1]; + + expect(funcTask.dependsOn).toBeUndefined(); + }); + }); + + describe('customCode project', () => { + it('should generate same structure as codeless', () => { + const codeless = generateTasksJson({ + projectType: ProjectType.logicApp, + projectPackageType: ProjectPackageType.Bundle, + hasFuncBinaries: true, + }); + const customCode = generateTasksJson({ + projectType: ProjectType.customCode, + projectPackageType: ProjectPackageType.Bundle, + hasFuncBinaries: true, + }); + + expect(customCode.tasks).toHaveLength(codeless.tasks.length); + expect(customCode.tasks[0].label).toBe('generateDebugSymbols'); + expect(customCode.tasks[1].label).toBe('func: host start'); + expect(customCode.tasks[1].group).toEqual({ kind: 'build', isDefault: true }); + }); + }); + + describe('nuget project', () => { + const baseConfig: VSCodeProjectConfig = { + projectType: ProjectType.logicApp, + projectPackageType: ProjectPackageType.Nuget, + hasFuncBinaries: true, + targetFramework: TargetFramework.Net8, + }; + + it('should generate dotnet tasks with generateDebugSymbols', () => { + const result = generateTasksJson(baseConfig); + + expect(result.tasks).toHaveLength(6); + const labels = result.tasks.map((t) => t.label); + expect(labels).toEqual(['generateDebugSymbols', 'clean', 'build', 'clean release', 'publish', 'func: host start']); + }); + + it('should have correct clean release args with clean subcommand', () => { + const result = generateTasksJson(baseConfig); + const cleanRelease = result.tasks.find((t) => t.label === 'clean release'); + + expect(cleanRelease.args).toEqual([ + 'clean', + '--configuration', + 'Release', + '/property:GenerateFullPaths=true', + '/consoleloggerparameters:NoSummary', + ]); + }); + + it('should have correct publish args', () => { + const result = generateTasksJson(baseConfig); + const publishTask = result.tasks.find((t) => t.label === 'publish'); + + expect(publishTask.args).toEqual([ + 'publish', + '--configuration', + 'Release', + '/property:GenerateFullPaths=true', + '/consoleloggerparameters:NoSummary', + ]); + expect(publishTask.dependsOn).toBe('clean release'); + }); + + it('should set func:host start to depend on build', () => { + const result = generateTasksJson(baseConfig); + const funcTask = result.tasks.find((t) => t.label === 'func: host start'); + + expect(funcTask.dependsOn).toBe('build'); + }); + + it('should not include group on func:host start for dotnet', () => { + const result = generateTasksJson(baseConfig); + const funcTask = result.tasks.find((t) => t.label === 'func: host start'); + + expect(funcTask.group).toBeUndefined(); + }); + + it('should include cwd in env options based on targetFramework', () => { + const result = generateTasksJson(baseConfig); + const funcTask = result.tasks.find((t) => t.label === 'func: host start'); + + expect((funcTask.options as any).cwd).toBe('bin/Debug/net8'); + }); + + it('should include inputs with getDebugSymbolDll', () => { + const result = generateTasksJson(baseConfig); + + expect(result.inputs).toHaveLength(1); + expect(result.inputs[0].id).toBe('getDebugSymbolDll'); + }); + }); + + describe('codeful project', () => { + it('should generate dotnet build tasks without generateDebugSymbols', () => { + const result = generateTasksJson({ + projectType: ProjectType.codeful, + projectPackageType: ProjectPackageType.Nuget, + hasFuncBinaries: true, + targetFramework: TargetFramework.Net8, + }); + + expect(result.tasks).toHaveLength(5); + const labels = result.tasks.map((t) => t.label); + expect(labels).toEqual(['clean', 'build', 'clean release', 'publish', 'func: host start']); + }); + + it('should not include inputs for codeful projects', () => { + const result = generateTasksJson({ + projectType: ProjectType.codeful, + projectPackageType: ProjectPackageType.Nuget, + hasFuncBinaries: true, + targetFramework: TargetFramework.Net8, + }); + + expect(result.inputs).toBeUndefined(); + }); + }); +}); diff --git a/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/extensionsGenerator.ts b/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/extensionsGenerator.ts new file mode 100644 index 00000000000..35969b9918a --- /dev/null +++ b/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/extensionsGenerator.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { csDevKitExtensionId, dotnetExtensionId, functionsExtensionId, logicAppsStandardExtensionId } from '../../../../constants'; + +export interface ExtensionsJsonContent { + recommendations: string[]; +} + +/** + * Generates the canonical extensions.json content for a Logic App project. + */ +export function generateExtensionsJson(): ExtensionsJsonContent { + const recommendations: string[] = [logicAppsStandardExtensionId, functionsExtensionId, dotnetExtensionId, csDevKitExtensionId]; + return { recommendations }; +} diff --git a/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/index.ts b/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/index.ts new file mode 100644 index 00000000000..5ebdf8e759e --- /dev/null +++ b/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/index.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +export * from './tasksGenerator'; +export * from './launchGenerator'; +export * from './settingsGenerator'; +export * from './extensionsGenerator'; +export * from './types'; diff --git a/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/launchGenerator.ts b/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/launchGenerator.ts new file mode 100644 index 00000000000..b00e20d33a9 --- /dev/null +++ b/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/launchGenerator.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import type { VSCodeProjectConfig } from './types'; +import { extensionCommand, launchVersion } from '../../../../constants'; +import { FuncVersion, ProjectType } from '@microsoft/vscode-extension-logic-apps'; +import { getDotnetRuntimeFromFramework, getDotnetRuntimeFromFunc } from '../../dotnet/dotnet'; +import type { DebugConfiguration } from 'vscode'; + +export interface LaunchJsonContent { + version: string; + configurations: DebugConfiguration[]; +} + +/** + * Generates the canonical launch.json content for a Logic App project. + */ +export function generateLaunchJson(config: VSCodeProjectConfig): LaunchJsonContent { + const logicAppName = config.logicAppName ?? 'logic app'; + return { + version: launchVersion, + configurations: [generateDebugConfiguration(config, logicAppName)], + }; +} + +/** + * Generates a single debug configuration based on the project type. + */ +function generateDebugConfiguration(config: VSCodeProjectConfig, logicAppName: string): DebugConfiguration { + const { projectType, customCodeTargetFramework, funcVersion } = config; + const version = funcVersion ?? FuncVersion.v4; + + if (projectType === ProjectType.codeful) { + return { + name: `Run/Debug logic app ${logicAppName}`, + type: 'logicapp', + request: 'launch', + funcRuntime: getDotnetRuntimeFromFunc(version), + isCodeless: false, + }; + } + + if (customCodeTargetFramework) { + return { + name: `Run/Debug logic app with local function ${logicAppName}`, + type: 'logicapp', + request: 'launch', + funcRuntime: getDotnetRuntimeFromFunc(version), + customCodeRuntime: getDotnetRuntimeFromFramework(customCodeTargetFramework), + isCodeless: true, + }; + } + + return { + name: `Run/Debug logic app ${logicAppName}`, + type: getDotnetRuntimeFromFunc(version), + request: 'attach', + processId: `\${command:${extensionCommand.pickProcess}}`, + }; +} diff --git a/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/settingsGenerator.ts b/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/settingsGenerator.ts new file mode 100644 index 00000000000..411aa4209f7 --- /dev/null +++ b/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/settingsGenerator.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import type { VSCodeProjectConfig } from './types'; +import { deploySubpathSetting, funcVersionSetting, projectLanguageSetting } from '../../../../constants'; +import { latestGAVersion, ProjectLanguage, ProjectPackageType, ProjectType, TargetFramework } from '@microsoft/vscode-extension-logic-apps'; +import * as path from 'path'; +import { ext } from '../../../../extensionVariables'; + +/** + * Generates the canonical settings.json content for a Logic App project. + */ +export function generateSettingsJson(config: VSCodeProjectConfig): Record { + const { projectType, projectPackageType, funcVersion, language, targetFramework } = config; + const resolvedLanguage = language ?? (projectType === ProjectType.codeful ? ProjectLanguage.CSharp : ProjectLanguage.JavaScript); + const resolvedVersion = funcVersion ?? latestGAVersion; + + const baseSettings: Record = { + [`${ext.prefix}.${projectLanguageSetting}`]: resolvedLanguage, + [`${ext.prefix}.${funcVersionSetting}`]: resolvedVersion, + 'debug.internalConsoleOptions': 'neverOpen', + 'azureFunctions.suppressProject': true, + }; + + if (projectType === ProjectType.codeful) { + const deploySubPathValue = path.posix.join('bin', 'Release', targetFramework ?? TargetFramework.NetFx, 'publish'); + return { + ...baseSettings, + 'azureFunctions.deploySubpath': deploySubPathValue, + 'azureFunctions.preDeployTask': 'publish', + 'azureFunctions.projectSubpath': deploySubPathValue, + 'omnisharp.enableMsBuildLoadProjectsOnDemand': false, + 'omnisharp.disableMSBuildDiagnosticWarning': true, + }; + } + + if (projectPackageType === ProjectPackageType.Nuget) { + const deploySubPathValue = path.posix.join('bin', 'Release', targetFramework ?? TargetFramework.NetFx, 'publish'); + return { + ...baseSettings, + [`${ext.prefix}.${deploySubpathSetting}`]: deploySubPathValue, + 'azureFunctions.preDeployTask': 'publish', + }; + } + + if (projectType === ProjectType.logicApp) { + return { + ...baseSettings, + [`${ext.prefix}.${deploySubpathSetting}`]: '.', + }; + } + + return baseSettings; +} diff --git a/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/tasksGenerator.ts b/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/tasksGenerator.ts new file mode 100644 index 00000000000..8dfc90a6358 --- /dev/null +++ b/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/tasksGenerator.ts @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import type { TasksJsonContent, VSCodeProjectConfig } from './types'; +import { getFuncHostTaskEnv } from '../../codeless/funcHostTaskEnv'; +import { dotnetPublishTaskLabel, extensionCommand, func, funcWatchProblemMatcher, hostStartCommand } from '../../../../constants'; +import { ProjectType, ProjectPackageType } from '@microsoft/vscode-extension-logic-apps'; +import * as path from 'path'; + +const TASKS_VERSION = '2.0.0'; +const FUNC_BINARY_PATH = '${config:azureLogicAppsStandard.funcCoreToolsBinaryPath}'; +const DOTNET_BINARY_PATH = '${config:azureLogicAppsStandard.dotnetBinaryPath}'; +const COMMON_DOTNET_ARGS = ['/property:GenerateFullPaths=true', '/consoleloggerparameters:NoSummary']; +const RELEASE_ARGS = ['--configuration', 'Release']; + +/** + * Generates the canonical tasks.json content for a Logic App project. + */ +export function generateTasksJson(config: VSCodeProjectConfig): TasksJsonContent { + const { projectType, projectPackageType } = config; + + if (projectType === ProjectType.codeful) { + return generateCodefulTasksJson(config); + } + + if (projectPackageType === ProjectPackageType.Nuget) { + return generateNugetTasksJson(config); + } + + return generateCodelessTasksJson(config); +} + +function generateCodefulTasksJson(config: VSCodeProjectConfig): TasksJsonContent { + return { + version: TASKS_VERSION, + tasks: [...getDotnetBuildTasks(), getFuncHostStartTask(config, { dependsOn: 'build' })], + }; +} + +function generateNugetTasksJson(config: VSCodeProjectConfig): TasksJsonContent { + return { + version: TASKS_VERSION, + tasks: [getDebugSymbolsTask(), ...getDotnetBuildTasks(), getFuncHostStartTask(config, { dependsOn: 'build' })], + inputs: [getDebugSymbolDllInput()], + }; +} + +function generateCodelessTasksJson(config: VSCodeProjectConfig): TasksJsonContent { + return { + version: TASKS_VERSION, + tasks: [getDebugSymbolsTask(), getFuncHostStartTask(config)], + inputs: [getDebugSymbolDllInput()], + }; +} + +function getDotnetBuildTasks() { + return [ + { + label: 'clean', + command: DOTNET_BINARY_PATH, + args: ['clean', ...COMMON_DOTNET_ARGS], + type: 'process', + problemMatcher: '$msCompile', + }, + { + label: 'build', + command: DOTNET_BINARY_PATH, + args: ['build', ...COMMON_DOTNET_ARGS], + type: 'process', + dependsOn: 'clean', + group: { + kind: 'build', + isDefault: true, + }, + problemMatcher: '$msCompile', + }, + { + label: 'clean release', + command: DOTNET_BINARY_PATH, + args: ['clean', ...RELEASE_ARGS, ...COMMON_DOTNET_ARGS], + type: 'process', + problemMatcher: '$msCompile', + }, + { + label: dotnetPublishTaskLabel, + command: DOTNET_BINARY_PATH, + args: ['publish', ...RELEASE_ARGS, ...COMMON_DOTNET_ARGS], + type: 'process', + dependsOn: 'clean release', + problemMatcher: '$msCompile', + }, + ]; +} + +function getDebugSymbolsTask() { + return { + label: 'generateDebugSymbols', + command: DOTNET_BINARY_PATH, + args: ['${input:getDebugSymbolDll}'], + type: 'process', + problemMatcher: '$msCompile', + }; +} + +function getFuncHostStartTask(config: VSCodeProjectConfig, options?: { dependsOn?: string }) { + const { hasFuncBinaries, isDevContainer, targetFramework, projectType, projectPackageType } = config; + const isDotnet = projectType === ProjectType.codeful || projectPackageType === ProjectPackageType.Nuget; + const debugSubpath = isDotnet && targetFramework ? path.posix.join('bin', 'Debug', targetFramework) : undefined; + + const envOptions = hasFuncBinaries && !isDevContainer ? getFuncHostTaskEnv(debugSubpath ? { cwd: debugSubpath } : undefined) : {}; + + const task: Record = { + label: 'func: host start', + type: hasFuncBinaries ? 'shell' : func, + command: hasFuncBinaries ? FUNC_BINARY_PATH : hostStartCommand, + args: hasFuncBinaries ? ['host', 'start'] : undefined, + ...envOptions, + problemMatcher: funcWatchProblemMatcher, + isBackground: true, + }; + + if (options?.dependsOn) { + task.dependsOn = options.dependsOn; + } + + if (!isDotnet) { + task.group = { kind: 'build', isDefault: true }; + } + + return task; +} + +function getDebugSymbolDllInput() { + return { + id: 'getDebugSymbolDll', + type: 'command', + command: extensionCommand.getDebugSymbolDll, + }; +} diff --git a/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/types.ts b/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/types.ts new file mode 100644 index 00000000000..00128064521 --- /dev/null +++ b/apps/vs-code-designer/src/app/utils/vsCodeConfig/generators/types.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import type { + FuncVersion, + ProjectLanguage, + ProjectPackageType, + ProjectType, + TargetFramework, +} from '@microsoft/vscode-extension-logic-apps'; + +export interface VSCodeProjectConfig { + projectType: ProjectType; + projectPackageType: ProjectPackageType; + hasFuncBinaries: boolean; + targetFramework?: TargetFramework; + isDevContainer?: boolean; + logicAppName?: string; + funcVersion?: FuncVersion; + language?: ProjectLanguage; + customCodeTargetFramework?: TargetFramework; +} + +export interface TasksJsonContent { + version: string; + tasks: Record[]; + inputs?: TaskInputJson[]; +} + +export interface TaskDefinitionJson { + label: string; + type: string; + command?: string; + args?: string[]; + problemMatcher: string; + isBackground?: boolean; + dependsOn?: string; + group?: { kind: string; isDefault: boolean }; + options?: Record; + windows?: Record; + linux?: Record; + osx?: Record; + [key: string]: unknown; +} + +export interface TaskInputJson { + id: string; + type: string; + command: string; +} 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 ba285bb9156..e9750b73dce 100644 --- a/apps/vs-code-designer/src/app/utils/vsCodeConfig/tasks.ts +++ b/apps/vs-code-designer/src/app/utils/vsCodeConfig/tasks.ts @@ -4,17 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { ext } from '../../../extensionVariables'; import { localize } from '../../../localize'; -import type { ProjectFile } from '../dotnet/dotnet'; -import { getDotnetDebugSubpath, getProjFiles, getTargetFramework } from '../dotnet/dotnet'; -import { getFuncHostTaskEnv } from '../codeless/funcHostTaskEnv'; +import { binariesExistSync } from '../binaries'; +import { detectProjectType, detectProjectPackageType } from '../project'; import { tryGetLogicAppProjectRoot } from '../verifyIsProject'; +import { generateTasksJson } from './generators'; import { DialogResponses, openUrl, type IActionContext } from '@microsoft/vscode-azext-utils'; -import { ProjectLanguage, type ITask, type ITaskInputs } from '@microsoft/vscode-extension-logic-apps'; +import { ProjectPackageType, ProjectType, type ITask, type ITaskInputs } from '@microsoft/vscode-extension-logic-apps'; import * as fse from 'fs-extra'; import * as path from 'path'; import { workspace } from 'vscode'; import type { MessageItem, TaskDefinition, WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; -import { tasksFileName, vscodeFolderName } from '../../../constants'; +import { funcDependencyName, tasksFileName, vscodeFolderName } from '../../../constants'; +import { tryGetTargetFramework } from '../dotnet/dotnet'; const tasksKey = 'tasks'; const inputsKey = 'inputs'; @@ -140,145 +141,20 @@ async function overwriteTasksJson(context: IActionContext, projectPath: string): '\n\nContinue with the update?'; const tasksJsonPath: string = path.join(projectPath, vscodeFolderName, tasksFileName); - let tasksJsonContent: any; + const projectType = await detectProjectType(projectPath); + const projectPackageType = await detectProjectPackageType(projectPath); - 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']; + const targetFramework = projectType === ProjectType.codeful || projectPackageType === ProjectPackageType.Nuget + ? await tryGetTargetFramework(projectPath) + : undefined; - let projFile: ProjectFile; - const projFiles = [ - ...(await getProjFiles(context, ProjectLanguage.FSharp, projectPath)), - ...(await getProjFiles(context, ProjectLanguage.CSharp, projectPath)), - ]; + const tasksJsonContent = generateTasksJson({ + projectType, + projectPackageType: projectPackageType, + hasFuncBinaries: binariesExistSync(funcDependencyName), + targetFramework, + }); - 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'], - ...getFuncHostTaskEnv({ cwd: debugSubpath }), - 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'], - ...getFuncHostTaskEnv(), - 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)); } diff --git a/apps/vs-code-designer/src/assets/WorkspaceTemplates/DevContainerTasksJsonFile b/apps/vs-code-designer/src/assets/WorkspaceTemplates/DevContainerTasksJsonFile deleted file mode 100644 index 72b95bbfd10..00000000000 --- a/apps/vs-code-designer/src/assets/WorkspaceTemplates/DevContainerTasksJsonFile +++ /dev/null @@ -1,36 +0,0 @@ -{ - "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" - ], - "problemMatcher": "$func-watch", - "isBackground": true, - "label": "func: host start", - "group": { - "kind": "build", - "isDefault": true - } - } - ], - "inputs": [ - { - "id": "getDebugSymbolDll", - "type": "command", - "command": "azureLogicAppsStandard.getDebugSymbolDll" - } - ] -} \ No newline at end of file diff --git a/apps/vs-code-designer/src/assets/WorkspaceTemplates/ExtensionsJsonFile b/apps/vs-code-designer/src/assets/WorkspaceTemplates/ExtensionsJsonFile deleted file mode 100644 index 97bf122e21b..00000000000 --- a/apps/vs-code-designer/src/assets/WorkspaceTemplates/ExtensionsJsonFile +++ /dev/null @@ -1,8 +0,0 @@ -{ - "recommendations": [ - "ms-azuretools.vscode-azurelogicapps", - "ms-azuretools.vscode-azurefunctions", - "ms-dotnettools.csharp", - "ms-dotnettools.csdevkit" - ] -} diff --git a/apps/vs-code-designer/src/assets/WorkspaceTemplates/TasksJsonFile b/apps/vs-code-designer/src/assets/WorkspaceTemplates/TasksJsonFile deleted file mode 100644 index dde1cc59b7a..00000000000 --- a/apps/vs-code-designer/src/assets/WorkspaceTemplates/TasksJsonFile +++ /dev/null @@ -1,62 +0,0 @@ -{ - "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": "${env:PATH}" - } - }, - "windows": { - "options": { - "env": { - "PATH": "${config:azureLogicAppsStandard.autoRuntimeDependenciesPath}\\NodeJs;${config:azureLogicAppsStandard.autoRuntimeDependenciesPath}\\DotNetSDK;${env:PATH}" - } - } - }, - "linux": { - "options": { - "env": { - "PATH": "${config:azureLogicAppsStandard.autoRuntimeDependenciesPath}/NodeJs:${config:azureLogicAppsStandard.autoRuntimeDependenciesPath}/DotNetSDK:${env:PATH}" - } - } - }, - "osx": { - "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" - } - ] -} \ No newline at end of file diff --git a/apps/vs-code-designer/src/constants.ts b/apps/vs-code-designer/src/constants.ts index c8a72bbcc9b..b901ca0cece 100644 --- a/apps/vs-code-designer/src/constants.ts +++ b/apps/vs-code-designer/src/constants.ts @@ -59,8 +59,11 @@ export const testMockClassTemplateName = 'TestMockClass'; export const testCsprojFileTemplateName = 'TestCsprojFile'; export const testSettingsConfigFileTemplateName = 'TestSettingsConfigFile'; -// Extension Id +// VS Code Extensions export const logicAppsStandardExtensionId = 'ms-azuretools.vscode-azurelogicapps'; +export const functionsExtensionId = 'ms-azuretools.vscode-azurefunctions'; +export const dotnetExtensionId = 'ms-dotnettools.csharp'; +export const csDevKitExtensionId = 'ms-dotnettools.csdevkit'; // Azurite export const azuriteExtensionId = 'Azurite.azurite'; @@ -69,7 +72,6 @@ export const azuriteLocationSetting = 'location'; // Functions export const func = 'func'; -export const functionsExtensionId = 'ms-azuretools.vscode-azurefunctions'; export const hostStartCommand = 'host start'; export const hostStartTaskName = `${func}: ${hostStartCommand}`; export const funcPackageName = 'azure-functions-core-tools'; @@ -371,8 +373,6 @@ export const DotnetVersion = { } as const; export type DotnetVersion = (typeof DotnetVersion)[keyof typeof DotnetVersion]; -export const dotnetExtensionId = 'ms-dotnettools.csharp'; - // Packages Manager export const PackageManager = { npm: 'npm', diff --git a/apps/vs-code-designer/test-setup.ts b/apps/vs-code-designer/test-setup.ts index d2d4bc98d7f..363102cc58b 100644 --- a/apps/vs-code-designer/test-setup.ts +++ b/apps/vs-code-designer/test-setup.ts @@ -139,7 +139,12 @@ vi.mock('vscode', () => ({ readFile: vi.fn(), readDirectory: vi.fn(), }, - getConfiguration: vi.fn(), + getConfiguration: vi.fn().mockReturnValue({ + get: vi.fn(), + has: vi.fn().mockReturnValue(false), + inspect: vi.fn(), + update: vi.fn(), + }), }, Uri: { file: (p: string) => ({ fsPath: p, toString: () => p }), diff --git a/libs/vscode-extension/src/lib/models/project.ts b/libs/vscode-extension/src/lib/models/project.ts index d23e8ed6b68..c7f339b3c9e 100644 --- a/libs/vscode-extension/src/lib/models/project.ts +++ b/libs/vscode-extension/src/lib/models/project.ts @@ -2,7 +2,7 @@ import type { IWorkerRuntime } from './cliFeed'; import type { FuncVersion } from './functions'; import type { IParsedHostJson } from './host'; import type { ProjectLanguage } from './language'; -import type { TargetFramework, WorkflowProjectType, WorkflowType } from './workflow'; +import type { TargetFramework, ProjectPackageType, WorkflowType } from './workflow'; import type { IActionContext } from '@microsoft/vscode-azext-utils'; import type { Uri, WorkspaceFolder } from 'vscode'; @@ -67,7 +67,6 @@ export interface IProjectWizardContext extends IActionContext { functionAppName?: string; customCodeFunctionName?: string; functionFolderPath?: string; - logicAppFolderPath?: string; projectPath: string; version: FuncVersion; workspacePath: string; @@ -80,7 +79,7 @@ export interface IProjectWizardContext extends IActionContext { workerRuntime?: IWorkerRuntime; openBehavior?: OpenBehavior; workspaceName?: string; - workflowProjectType?: WorkflowProjectType; + projectPackageType?: ProjectPackageType; generateFromOpenAPI?: boolean; openApiSpecificationFile?: Uri[]; targetFramework?: TargetFramework; diff --git a/libs/vscode-extension/src/lib/models/workflow.ts b/libs/vscode-extension/src/lib/models/workflow.ts index 43494325d28..d0f47eb1079 100644 --- a/libs/vscode-extension/src/lib/models/workflow.ts +++ b/libs/vscode-extension/src/lib/models/workflow.ts @@ -83,12 +83,11 @@ export interface ICallbackUrlResponse { queries?: Record; } -export const WorkflowProjectType = { +export const ProjectPackageType = { Nuget: 'Nuget', Bundle: 'Bundle', - Functions: 'Functions', } as const; -export type WorkflowProjectType = (typeof WorkflowProjectType)[keyof typeof WorkflowProjectType]; +export type ProjectPackageType = (typeof ProjectPackageType)[keyof typeof ProjectPackageType]; export interface ISettingToAdd { key: string;