diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/ConvertToWorkspace.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/ConvertToWorkspace.ts new file mode 100644 index 00000000000..e8110fcc047 --- /dev/null +++ b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/ConvertToWorkspace.ts @@ -0,0 +1,71 @@ +import { extensionCommand, funcVersionSetting } from '../../../../constants'; +import { localize } from '../../../../localize'; +import { addLocalFuncTelemetry, tryGetLocalFuncVersion, tryParseFuncVersion } from '../../../utils/funcCoreTools/funcVersion'; +import { getGlobalSetting } from '../../../utils/vsCodeConfig/settings'; +import { FolderListStep } from '../../createNewProject/createProjectSteps/FolderListStep'; +import { OpenFolderStepCodeProject } from './OpenFolderStepCodeProject'; +import { AzureWizard, DialogResponses } from '@microsoft/vscode-azext-utils'; +import type { IActionContext } from '@microsoft/vscode-azext-utils'; +import { latestGAVersion } from '@microsoft/vscode-extension-logic-apps'; +import type { IFunctionWizardContext } from '@microsoft/vscode-extension-logic-apps'; +import * as vscode from 'vscode'; +import { window } from 'vscode'; +import { getWorkspaceFile, getWorkspaceFileInParentDirectory, getWorkspaceFolder, getWorkspaceRoot } from '../../../utils/workspace'; +import { SetWorkspaceName } from './SetWorkspaceName'; +import { SetWorkspaceContents } from './SetWorkspaceContents'; +import { isLogicAppProjectInRoot } from '../../../utils/verifyIsProject'; + +export async function ConvertToWorkspace(context: IActionContext): Promise { + const workspaceFolder = await getWorkspaceFolder(context, undefined, true); + if (await isLogicAppProjectInRoot(workspaceFolder)) { + addLocalFuncTelemetry(context); + + const version: string = getGlobalSetting(funcVersionSetting) || (await tryGetLocalFuncVersion()) || latestGAVersion; + const wizardContext: Partial & IActionContext = Object.assign(context, { + version: tryParseFuncVersion(version), + }); + context.telemetry.properties.isWorkspace = 'false'; + wizardContext.workspaceCustomFilePath = + (await getWorkspaceFile(wizardContext)) ?? (await getWorkspaceFileInParentDirectory(wizardContext)); + //save uri variable for open project folder command + wizardContext.customWorkspaceFolderPath = await getWorkspaceRoot(wizardContext); + if (wizardContext.workspaceCustomFilePath && !wizardContext.customWorkspaceFolderPath) { + const message = localize( + 'openContainingWorkspace', + `You must open your workspace to use the full functionality in the Azure Logic Apps (Standard) extension. You can find the workspace with your logic app project at the following location: ${wizardContext.workspaceCustomFilePath}. Do you want to open this workspace now?` + ); + const result = await vscode.window.showInformationMessage(message, { modal: true }, DialogResponses.yes, DialogResponses.no); + if (result === DialogResponses.yes) { + await vscode.commands.executeCommand(extensionCommand.vscodeOpenFolder, vscode.Uri.file(wizardContext.workspaceCustomFilePath)); + context.telemetry.properties.openContainingWorkspace = 'true'; + return true; + } + context.telemetry.properties.openContainingWorkspace = 'false'; + return false; + } + if (!wizardContext.workspaceCustomFilePath && !wizardContext.customWorkspaceFolderPath) { + const message = localize( + 'createContainingWorkspace', + 'Your logic app projects must exist inside a workspace to use the full functionality in the Azure Logic Apps (Standard) extension. Visual Studio Code will copy your projects to a new workspace. Do you want to create the workspace now?' + ); + const result = await vscode.window.showInformationMessage(message, { modal: true }, DialogResponses.yes, DialogResponses.no); + if (result === DialogResponses.yes) { + const workspaceWizard: AzureWizard = new AzureWizard(wizardContext, { + title: localize('convertToWorkspace', 'Convert to workspace'), + promptSteps: [new FolderListStep(), new SetWorkspaceName(), new SetWorkspaceContents()], + executeSteps: [new OpenFolderStepCodeProject()], + }); + + await workspaceWizard.prompt(); + await workspaceWizard.execute(); + context.telemetry.properties.createContainingWorkspace = 'true'; + window.showInformationMessage(localize('finishedConvertingWorkspace', 'Finished converting to workspace.')); + return true; + } + context.telemetry.properties.createContainingWorkspace = 'false'; + return false; + } + context.telemetry.properties.isWorkspace = 'true'; + return true; + } +} diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/CreateNewProjectInternal.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/CreateNewProjectInternal.ts new file mode 100644 index 00000000000..1c4e8d913a3 --- /dev/null +++ b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/CreateNewProjectInternal.ts @@ -0,0 +1,75 @@ +import { funcVersionSetting, projectLanguageSetting, projectOpenBehaviorSetting, projectTemplateKeySetting } from '../../../../constants'; +import { localize } from '../../../../localize'; +import { createArtifactsFolder } from '../../../utils/codeless/artifacts'; +import { addLocalFuncTelemetry, tryGetLocalFuncVersion, tryParseFuncVersion } from '../../../utils/funcCoreTools/funcVersion'; +import { getGlobalSetting, getWorkspaceSetting } from '../../../utils/vsCodeConfig/settings'; +import { FolderListStep } from '../../createNewProject/createProjectSteps/FolderListStep'; +import { OpenFolderStepCodeProject } from './OpenFolderStepCodeProject'; +import { AzureWizard } from '@microsoft/vscode-azext-utils'; +import type { IActionContext } from '@microsoft/vscode-azext-utils'; +import { latestGAVersion, OpenBehavior, ProjectType } from '@microsoft/vscode-extension-logic-apps'; +import type { ICreateFunctionOptions, IFunctionWizardContext, ProjectLanguage } from '@microsoft/vscode-extension-logic-apps'; +import * as fse from 'fs-extra'; +import * as path from 'path'; +import { window } from 'vscode'; + +export async function createRulesFiles(context: IFunctionWizardContext): Promise { + if (context.projectType === ProjectType.rulesEngine) { + const xmlTemplatePath = path.join(__dirname, 'assets', 'RuleSetProjectTemplate', 'SampleRuleSet'); + const xmlRuleSetPath = path.join(context.projectPath, 'Artifacts', 'Rules', 'SampleRuleSet.xml'); + const xmlTemplateContent = await fse.readFile(xmlTemplatePath, 'utf-8'); + const xmlFileContent = xmlTemplateContent.replace(/<%= methodName %>/g, context.methodName); + await fse.writeFile(xmlRuleSetPath, xmlFileContent); + } +} + +export async function createLibFolder(context: IFunctionWizardContext): Promise { + fse.mkdirSync(path.join(context.projectPath, 'lib', 'builtinOperationSdks', 'JAR'), { recursive: true }); + fse.mkdirSync(path.join(context.projectPath, 'lib', 'builtinOperationSdks', 'net472'), { recursive: true }); + fse.mkdirSync(path.join(context.projectPath, 'lib', 'custom', 'net472'), { recursive: true }); +} + +export async function createNewProjectInternalBase( + context: IActionContext, + options: ICreateFunctionOptions, + title: string, + message: string, + promptSteps: any[] +): Promise { + addLocalFuncTelemetry(context); + + const language: ProjectLanguage | string = (options.language as ProjectLanguage) || getGlobalSetting(projectLanguageSetting); + const version: string = options.version || getGlobalSetting(funcVersionSetting) || (await tryGetLocalFuncVersion()) || latestGAVersion; + const projectTemplateKey: string | undefined = getGlobalSetting(projectTemplateKeySetting); + const wizardContext: Partial & IActionContext = Object.assign(context, options, { + language, + version: tryParseFuncVersion(version), + projectTemplateKey, + }); + + if (options.folderPath) { + FolderListStep.setProjectPath(wizardContext, options.folderPath); + } + + if (options.suppressOpenFolder) { + wizardContext.openBehavior = OpenBehavior.dontOpen; + } else if (!wizardContext.openBehavior) { + wizardContext.openBehavior = getWorkspaceSetting(projectOpenBehaviorSetting); + context.telemetry.properties.openBehaviorFromSetting = String(!!wizardContext.openBehavior); + } + + const wizard: AzureWizard = new AzureWizard(wizardContext, { + title: localize(title, message), + promptSteps, + executeSteps: [new OpenFolderStepCodeProject()], + }); + + await wizard.prompt(); + await wizard.execute(); + + await createArtifactsFolder(context as IFunctionWizardContext); + await createRulesFiles(context as IFunctionWizardContext); + await createLibFolder(context as IFunctionWizardContext); + + window.showInformationMessage(localize('finishedCreating', 'Finished creating project.')); +} diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/NewCodeProjectTypeStep.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/NewCodeProjectTypeStep.ts index 5d44b347d2b..7e5f8686709 100644 --- a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/NewCodeProjectTypeStep.ts +++ b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/NewCodeProjectTypeStep.ts @@ -21,7 +21,7 @@ export class NewCodeProjectTypeStep extends AzureWizardPromptStep { + private async setPaths(context: IProjectWizardContext, workspacePath: string): Promise { await fs.ensureDir(workspacePath); - context.customWorkspaceFolderPath = workspacePath; - let logicAppFolderName = 'LogicApp'; - if (!isWorkspaceWithFunctions && context.logicAppName) { - logicAppFolderName = context.logicAppName; - } + const logicAppFolderName = context.logicAppName; const logicAppFolderPath = path.join(workspacePath, logicAppFolderName); await fs.ensureDir(logicAppFolderPath); context.logicAppFolderPath = logicAppFolderPath; context.projectPath = logicAppFolderPath; - context.workspacePath = logicAppFolderPath; - - if (isWorkspaceWithFunctions) { - await this.setupCustomDirectories(context, workspacePath); - } - await this.createWorkspaceFile(context); - } - /** - * Setup directories and configs for custom code logic app - * @param context - Project wizard context - * @param workspacePath - Root path of the workspace - */ - private async setupCustomDirectories(context: IProjectWizardContext, workspacePath: string): Promise { - const functionFolderPath = path.join(workspacePath, 'Function'); - await fs.ensureDir(functionFolderPath); - context.functionFolderPath = functionFolderPath; } /** @@ -169,31 +146,4 @@ export class NewCodeProjectTypeStep extends AzureWizardPromptStep { - // Start with an empty folders array - const workspaceFolders = []; - - // Add Functions folder first if it's a custom code code Logic App - if (context.isWorkspaceWithFunctions) { - workspaceFolders.push({ name: 'Functions', path: './Function' }); - } - - // Use context.logicAppName for the folder name; default to 'LogicApp' if not available - const logicAppName = context.logicAppName || 'LogicApp'; - workspaceFolders.push({ name: logicAppName, path: `./${logicAppName}` }); - - const workspaceData = { - folders: workspaceFolders, - }; - - const workspaceFilePath = path.join(context.customWorkspaceFolderPath, `${context.workspaceName}.code-workspace`); - context.customWorkspaceFolderPath = workspaceFilePath; - - await fs.writeJSON(workspaceFilePath, workspaceData, { spaces: 2 }); - } } diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/OpenFolderStepCodeProject.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/OpenFolderStepCodeProject.ts index 0029dda15bd..48fa15881cb 100644 --- a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/OpenFolderStepCodeProject.ts +++ b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/OpenFolderStepCodeProject.ts @@ -23,7 +23,7 @@ export class OpenFolderStepCodeProject extends AzureWizardExecuteStep { - // Set the priority of the step - public priority = 10; - - /** - * Executes the core logic for the step. - * @param context The project wizard context. - * @param progress The progress object to report progress updates to. - */ - public abstract executeCore( - context: IProjectWizardContext, - progress: Progress<{ message?: string | undefined; increment?: number | undefined }> - ): Promise; - - /** - * Creates the project directory and executes the core logic for the step. - * @param context The project wizard context. - * @param progress The progress object to report progress updates to. - */ - public async execute( - context: IProjectWizardContext, - progress: Progress<{ message?: string | undefined; increment?: number | undefined }> - ): Promise { - // Set telemetry properties - context.telemetry.properties.projectLanguage = context.language; - context.telemetry.properties.projectRuntime = context.version; - context.telemetry.properties.openBehavior = context.openBehavior; - - // Create the project directory - progress.report({ message: localize('creating', 'Creating new project...') }); - await fse.ensureDir(context.projectPath); - - // Execute the core logic for the step - await this.executeCore(context, progress); - - // Initialize a git repository if one is not already present - if ((await isGitInstalled(context.customWorkspaceFolderPath)) && !(await isInsideRepo(context.customWorkspaceFolderPath))) { - await gitInit(context.customWorkspaceFolderPath); - } - - // Log telemetry for the step - await callWithTelemetryAndErrorHandling('azureLogicAppsStandard.createNewCodeProjectStarted', (startedContext: IActionContext) => { - Object.assign(startedContext, context); - }); - } - - /** - * Determines whether the step should be executed. - * @param context The project wizard context. - * @returns True if the step should be executed, false otherwise. - */ - public shouldExecute(_context: IProjectWizardContext): boolean { - return true; - } -} diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/SetLogicAppNameStep.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/SetLogicAppNameStep.ts index f039ba8c702..f469080145c 100644 --- a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/SetLogicAppNameStep.ts +++ b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/SetLogicAppNameStep.ts @@ -4,27 +4,39 @@ *--------------------------------------------------------------------------------------------*/ 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 { ProjectType, type IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps'; +import type { IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps'; export class SetLogicAppName extends AzureWizardPromptStep { public async prompt(context: IProjectWizardContext): Promise { - const logicAppName = await context.ui.showInputBox({ + context.logicAppName = await context.ui.showInputBox({ placeHolder: localize('logicAppNamePlaceHolder', 'Logic App name'), prompt: localize('logicAppNamePrompt', 'Enter a name for your Logic App project'), - validateInput: (value: string): string | undefined => { - if (!value || value.length === 0) { - return localize('projectNameEmpty', 'Project name cannot be empty'); - } - return undefined; - }, + validateInput: async (input: string): Promise => await this.validateLogicAppName(input, context), }); - context.logicAppName = logicAppName; - ext.outputChannel.appendLog(localize('logicAppNameSet', `Logic App project name set to ${logicAppName}`)); + ext.outputChannel.appendLog(localize('logicAppNameSet', `Logic App project name set to ${context.logicAppName}`)); } public shouldPrompt(context: IProjectWizardContext): boolean { - return context.projectType === ProjectType.logicApp; + return context.projectType !== undefined; + } + + private async validateLogicAppName(name: string | undefined, context: IProjectWizardContext): Promise { + if (!name || name.length === 0) { + return localize('projectNameEmpty', 'Project name cannot be empty'); + } + + if (fse.existsSync(context.workspaceCustomFilePath)) { + const workspaceFileContent = await vscode.workspace.fs.readFile(vscode.Uri.file(context.workspaceCustomFilePath)); + const workspaceFileJson = JSON.parse(workspaceFileContent.toString()); + + if (workspaceFileJson.folders && workspaceFileJson.folders.some((folder: { path: string }) => folder.path === `./${name}`)) { + return localize('projectNameExists', 'A project with this name already exists in the workspace'); + } + } + return undefined; } } diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/SetWorkspaceContents.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/SetWorkspaceContents.ts new file mode 100644 index 00000000000..bfb9059fd24 --- /dev/null +++ b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/SetWorkspaceContents.ts @@ -0,0 +1,71 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { AzureWizardPromptStep } from '@microsoft/vscode-azext-utils'; +import { type IProjectWizardContext, ProjectLanguage, WorkflowProjectType } from '@microsoft/vscode-extension-logic-apps'; +import * as fse from 'fs-extra'; +import * as path from 'path'; +import { OpenBehavior } from '@microsoft/vscode-extension-logic-apps'; +import * as vscode from 'vscode'; +import { isLogicAppProject } from '../../../utils/verifyIsProject'; + +export class SetWorkspaceContents extends AzureWizardPromptStep { + // Hide the step count in the wizard UI + public hideStepCount = true; + + /** + * Prompts the user for project information and sets up directories + * @param context - Project wizard context containing user selections and settings + */ + public async prompt(context: IProjectWizardContext): Promise { + // Set default project type and language + context.workflowProjectType = WorkflowProjectType.Bundle; + context.language = ProjectLanguage.JavaScript; + await this.createWorkspaceFile(context); + } + + /** + * Checks if this step should prompt the user + * @param context - Project wizard context containing user selections and settings + * @returns True if user should be prompted, otherwise false + */ + public shouldPrompt(): boolean { + return true; + } + + /** + * Creates a .code-workspace file to group project directories in VS Code + * @param context - Project wizard context + */ + private async createWorkspaceFile(context: IProjectWizardContext): Promise { + // Start with an empty folders array + const workspaceFolders = []; + const foldersToAdd = vscode.workspace.workspaceFolders; + + if (foldersToAdd && foldersToAdd.length === 1) { + const folder = foldersToAdd[0]; + const folderPath = folder.uri.fsPath; + if (await isLogicAppProject(folderPath)) { + const destinationPath = path.join(context.workspacePath, folder.name); + await fse.copy(folderPath, destinationPath); + workspaceFolders.push({ name: folder.name, path: `./${folder.name}` }); + } else { + const subpaths: string[] = await fse.readdir(folderPath); + for (const subpath of subpaths) { + const fullPath = path.join(folderPath, subpath); + const destinationPath = path.join(context.workspacePath, subpath); + await fse.copy(fullPath, destinationPath); + workspaceFolders.push({ name: subpath, path: `./${subpath}` }); + } + } + } + + const workspaceData = { + folders: workspaceFolders, + }; + + await fse.writeJSON(context.workspaceCustomFilePath, workspaceData, { spaces: 2 }); + context.openBehavior = OpenBehavior.openInCurrentWindow; + } +} diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/SetWorkspaceName.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/SetWorkspaceName.ts index 8754bebcc2b..79494c29994 100644 --- a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/SetWorkspaceName.ts +++ b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/SetWorkspaceName.ts @@ -10,7 +10,7 @@ import { OpenBehavior } from '@microsoft/vscode-extension-logic-apps'; import type { IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps'; import * as path from 'path'; -export class setWorkspaceName extends AzureWizardPromptStep { +export class SetWorkspaceName extends AzureWizardPromptStep { public hideStepCount = true; public async prompt(context: IProjectWizardContext): Promise { @@ -23,6 +23,8 @@ export class setWorkspaceName extends AzureWizardPromptStep { + // Hide the step count in the wizard UI + public hideStepCount = true; + + /** + * Prompts the user for project information and sets up directories + * @param context - Project wizard context containing user selections and settings + */ + public async prompt(context: IProjectWizardContext): Promise { + // Set default project type and language + context.workflowProjectType = WorkflowProjectType.Bundle; + context.language = ProjectLanguage.JavaScript; + // Create directories based on user choices + const { workspacePath, isWorkspaceWithFunctions } = context; + if (context.customWorkspaceFolderPath && context.workspaceCustomFilePath.endsWith('.code-workspace')) { + await this.createDirectoriesExistingWorkspace(context, workspacePath, isWorkspaceWithFunctions); + } else { + await this.createDirectories(context, workspacePath, isWorkspaceWithFunctions); + } + } + + /** + * Checks if this step should prompt the user + * @param context - Project wizard context containing user selections and settings + * @returns True if user should be prompted, otherwise false + */ + public shouldPrompt(): boolean { + return true; + } + + /** + * Creates required directories for the project + * @param context - Project wizard context + * @param workspacePath - Root path of the workspace + * @param isWorkspaceWithFunctions - Flag to check if it's a workspace with functions + */ + private async createDirectories(context: IProjectWizardContext, workspacePath: string, isWorkspaceWithFunctions: boolean): Promise { + context.customWorkspaceFolderPath = workspacePath; + if (isWorkspaceWithFunctions) { + await this.setupCustomDirectories(context, workspacePath); + } + await this.createWorkspaceFile(context); + } + + /** + * Creates required directories for the project + * @param context - Project wizard context + * @param workspacePath - Root path of the workspace + * @param isWorkspaceWithFunctions - Flag to check if it's a workspace with functions + */ + private async createDirectoriesExistingWorkspace( + context: IProjectWizardContext, + workspacePath: string, + isWorkspaceWithFunctions: boolean + ): Promise { + if (isWorkspaceWithFunctions) { + await this.setupCustomDirectories(context, workspacePath); + } + await this.updateWorkspaceFile(context); + context.openBehavior = OpenBehavior.addToWorkspace; + } + + /** + * Setup directories and configs for custom code logic app + * @param context - Project wizard context + * @param workspacePath - Root path of the workspace + */ + private async setupCustomDirectories(context: IProjectWizardContext, workspacePath: string): Promise { + const functionFolderPath = path.join(workspacePath, context.methodName); + await fs.ensureDir(functionFolderPath); + context.functionFolderPath = functionFolderPath; + } + + /** + * Creates a .code-workspace file to group project directories in VS Code + * @param context - Project wizard context + */ + private async createWorkspaceFile(context: IProjectWizardContext): Promise { + // Start with an empty folders array + const workspaceFolders = []; + + // Add Functions folder first if it's a custom code code Logic App + const functionsFolder = context.methodName; + if (context.isWorkspaceWithFunctions) { + workspaceFolders.push({ name: functionsFolder, path: `./${functionsFolder}` }); + } + + // Use context.logicAppName for the folder name; default to 'LogicApp' if not available + const logicAppName = context.logicAppName || 'LogicApp'; + workspaceFolders.push({ name: logicAppName, path: `./${logicAppName}` }); + + const workspaceData = { + folders: workspaceFolders, + }; + + await fs.writeJSON(context.workspaceCustomFilePath, workspaceData, { spaces: 2 }); + } + + /** + * Updates a .code-workspace file to group project directories in VS Code + * @param context - Project wizard context + */ + private async updateWorkspaceFile(context: IProjectWizardContext): Promise { + // Read existing workspace file + const workspaceContent = await fs.readJson(context.workspaceCustomFilePath); + + // Start with an empty folders array + const workspaceFolders = []; + + // Add Functions folder first if it's a custom code code Logic App + const functionsFolder = context.methodName; + if (context.isWorkspaceWithFunctions) { + workspaceFolders.push({ name: functionsFolder, path: `./${functionsFolder}` }); + } + + // Use context.logicAppName for the folder name; default to 'LogicApp' if not available + const logicAppName = context.logicAppName || 'LogicApp'; + workspaceFolders.push({ name: logicAppName, path: `./${logicAppName}` }); + + workspaceContent.folders = [...workspaceContent.folders, ...workspaceFolders]; + + await fs.writeJSON(context.workspaceCustomFilePath, workspaceContent, { spaces: 2 }); + } +} diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/WorkflowCodeProjectCreateStep.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/WorkflowCodeProjectCreateStep.ts index 45c2331cdf7..a5c455eeb9d 100644 --- a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/WorkflowCodeProjectCreateStep.ts +++ b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/WorkflowCodeProjectCreateStep.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ScriptProjectCreateStep } from '../createCodeProjectSteps/createLogicApp/ScriptProjectCreateStep'; +import { ScriptProjectCreateStep } from '../../createNewProject/createProjectSteps/ScriptProjectCreateStep'; import type { IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps'; import type { Progress } from 'vscode'; @@ -31,6 +31,8 @@ export class WorkflowCodeProjectCreateStep extends ScriptProjectCreateStep { progress: Progress<{ message?: string | undefined; increment?: number | undefined }> ): Promise { context.projectPath = this.projectPath; + this.funcignore.push('global.json'); + this.localSettingsJson.Values['AzureWebJobsFeatureFlags'] = 'EnableMultiLanguageWorker'; await super.executeCore(context, progress); } } diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/cloudToLocal.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/cloudToLocal.ts index d99a0dc6610..f1e48b5de86 100644 --- a/apps/vs-code-designer/src/app/commands/createNewCodeProject/cloudToLocal.ts +++ b/apps/vs-code-designer/src/app/commands/createNewCodeProject/cloudToLocal.ts @@ -14,7 +14,7 @@ import { NewCodeProjectTypeStep } from './CodeProjectBase/NewCodeProjectTypeStep import { SelectPackageStep } from '../createNewProject/createProjectSteps/SelectPackageStep'; import { OpenFolderStepCodeProject } from './CodeProjectBase/OpenFolderStepCodeProject'; import { SetLogicAppName } from './CodeProjectBase/SetLogicAppNameStep'; -import { setWorkspaceName } from './CodeProjectBase/SetWorkspaceName'; +import { SetWorkspaceName } from './CodeProjectBase/SetWorkspaceName'; import { AzureWizard } from '@microsoft/vscode-azext-utils'; import type { IActionContext } from '@microsoft/vscode-azext-utils'; import { latestGAVersion, OpenBehavior } from '@microsoft/vscode-extension-logic-apps'; @@ -62,7 +62,7 @@ export async function cloudToLocalCommand( promptSteps: [ new SelectPackageStep(), new SelectFolderForNewWorkspaceStep(), - new setWorkspaceName(), + new SetWorkspaceName(), new SetLogicAppName(), new NewCodeProjectTypeStep(options.templateId, options.functionSettings, true), new ExtractPackageStep(), diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createFunction/FunctionConfigFile.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createFunction/FunctionConfigFile.ts index 36c42a6d318..76221127525 100644 --- a/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createFunction/FunctionConfigFile.ts +++ b/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createFunction/FunctionConfigFile.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { dotnetExtensionId, functionsExtensionId } from '../../../../../constants'; +import { dotnetExtensionId, functionsExtensionId, vscodeFolderName } from '../../../../../constants'; import { AzureWizardPromptStep } from '@microsoft/vscode-azext-utils'; import type { IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps'; import * as fs from 'fs-extra'; @@ -24,7 +24,7 @@ export class FunctionConfigFile extends AzureWizardPromptStep { @@ -115,7 +119,10 @@ export class InvokeFunctionProjectSetup extends AzureWizardPromptStep/g, methodName); + const csprojFileContent = templateContent.replace( + /LogicApp<\/LogicAppFolder>/g, + `${logicAppName}` + ); await fs.writeFile(csprojFilePath, csprojFileContent); } diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createFunction/setMethodName.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createFunction/setMethodName.ts index 770c6bc6365..9be19c4acd4 100644 --- a/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createFunction/setMethodName.ts +++ b/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createFunction/setMethodName.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { localize } from '../../../../../localize'; +import * as 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'; @@ -13,7 +15,7 @@ export class setMethodName extends AzureWizardPromptStep context.methodName = await context.ui.showInputBox({ placeHolder: localize('setFunctioname', 'Function name'), prompt: localize('functionNamePrompt', 'Provide a function name for functions app project'), - validateInput: async (input: string): Promise => await this.validateFunctionName(input), + validateInput: async (input: string): Promise => await this.validateFunctionName(input, context), }); } @@ -21,12 +23,28 @@ export class setMethodName extends AzureWizardPromptStep return true; } - private async validateFunctionName(name: string | undefined): Promise { + private async validateFunctionName(name: string | undefined, context: IProjectWizardContext): Promise { if (!name) { - return localize('emptyTemplateNameError', 'The function name cannot be empty.'); + return localize('emptyTemplateNameError', `Can't have an empty function name.`); } if (!/^[a-z][a-z\d_]*$/i.test(name)) { - return localize('functionNameInvalidMessage', 'Function name must start with a letter and can only contain letters, digits and "_".'); + 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 name and the project name.`); + } + + if (fse.existsSync(context.workspaceCustomFilePath)) { + const workspaceFileContent = await vscode.workspace.fs.readFile(vscode.Uri.file(context.workspaceCustomFilePath)); + const workspaceFileJson = JSON.parse(workspaceFileContent.toString()); + + if (workspaceFileJson.folders && workspaceFileJson.folders.some((folder: { path: string }) => folder.path === `./${name}`)) { + return localize('functionNameExistsInWorkspaceError', 'A function with this name already exists in the workspace.'); + } } } } diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createLogicApp/ScriptProjectCreateStep.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createLogicApp/ScriptProjectCreateStep.ts deleted file mode 100644 index e147c21b405..00000000000 --- a/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createLogicApp/ScriptProjectCreateStep.ts +++ /dev/null @@ -1,117 +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 { - azureWebJobsStorageKey, - funcIgnoreFileName, - gitignoreFileName, - hostFileName, - localEmulatorConnectionString, - localSettingsFileName, - logicAppKind, -} from '../../../../../constants'; -import { addDefaultBundle } from '../../../../utils/bundleFeed'; -import { confirmOverwriteFile, writeFormattedJson } from '../../../../utils/fs'; -import { ProjectCodeCreateStepBase } from '../../CodeProjectBase/ProjectCodeCreateStepBase'; -import { nonNullProp } from '@microsoft/vscode-azext-utils'; -import type { IActionContext } from '@microsoft/vscode-azext-utils'; -import type { IHostJsonV1, IHostJsonV2, ILocalSettingsJson, IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps'; -import { FuncVersion } from '@microsoft/vscode-extension-logic-apps'; -import * as fse from 'fs-extra'; -import * as os from 'os'; -import * as path from 'path'; -import type { Progress } from 'vscode'; -import { getGitIgnoreContent } from '../../../../utils/git'; - -/** - * This class represents a step that creates the contents of a new script project. - */ -export class ScriptProjectCreateStep extends ProjectCodeCreateStepBase { - // Set of files that should be ignored when creating a script project. - protected funcignore: string[] = [ - '__blobstorage__', - '__queuestorage__', - '__azurite_db*__.json', - '.git*', - '.vscode', - localSettingsFileName, - 'test', - '.debug', - 'global.json', - ]; - protected gitignore = ''; - protected supportsManagedDependencies = false; - - /** - * Executes the step to create the contents of the project. - * @param context The project wizard context. - * @param _progress The progress object to use for reporting progress. - */ - public async executeCore( - context: IProjectWizardContext, - _progress: Progress<{ message?: string | undefined; increment?: number | undefined }> - ): Promise { - // Get the version of the Azure Functions runtime - const version: FuncVersion = nonNullProp(context, 'version'); - - // Create the host.json file - const hostJsonPath: string = path.join(context.projectPath, hostFileName); - if (await confirmOverwriteFile(context, hostJsonPath)) { - const hostJson: IHostJsonV2 | IHostJsonV1 = version === FuncVersion.v1 ? {} : await this.getHostContent(context); - await writeFormattedJson(hostJsonPath, hostJson); - } - - // Create the local.settings.json file - const localSettingsJsonPath: string = path.join(context.projectPath, localSettingsFileName); - if (await confirmOverwriteFile(context, localSettingsJsonPath)) { - const localSettingsJson: ILocalSettingsJson = { - IsEncrypted: false, - Values: { - [azureWebJobsStorageKey]: localEmulatorConnectionString, - WORKFLOWS_SUBSCRIPTION_ID: '', - FUNCTIONS_WORKER_RUNTIME: 'node', - APP_KIND: logicAppKind, - AzureWebJobsFeatureFlags: 'EnableMultiLanguageWorker', - }, - }; - await writeFormattedJson(localSettingsJsonPath, localSettingsJson); - } - - // Create the .gitignore file - const gitignorePath: string = path.join(context.projectPath, gitignoreFileName); - if (await confirmOverwriteFile(context, gitignorePath)) { - await fse.writeFile(gitignorePath, this.gitignore.concat(getGitIgnoreContent())); - } - - // Create the .funcignore file - const funcIgnorePath: string = path.join(context.projectPath, funcIgnoreFileName); - if (await confirmOverwriteFile(context, funcIgnorePath)) { - await fse.writeFile(funcIgnorePath, this.funcignore.sort().join(os.EOL)); - } - } - - /** - * Gets the contents of the host.json file for the specified context. - * @param context The action context. - * @returns The contents of the host.json file. - */ - protected async getHostContent(context: IActionContext): Promise { - const hostJson: IHostJsonV2 = { - version: '2.0', - logging: { - applicationInsights: { - samplingSettings: { - isEnabled: true, - excludedTypes: 'Request', - }, - }, - }, - }; - - // Add the default bundle to the host.json file - await addDefaultBundle(context, hostJson); - - return hostJson; - } -} diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createLogicApp/initLogicAppCodeProjectVScode/InitCodeProject.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createLogicApp/initLogicAppCodeProjectVScode/InitCodeProject.ts index f80a3d429ad..e560196052e 100644 --- a/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createLogicApp/initLogicAppCodeProjectVScode/InitCodeProject.ts +++ b/apps/vs-code-designer/src/app/commands/createNewCodeProject/createCodeProjectSteps/createLogicApp/initLogicAppCodeProjectVScode/InitCodeProject.ts @@ -15,6 +15,7 @@ import { extensionsFileName, extensionCommand, logicAppsStandardExtensionId, + vscodeFolderName, } from '../../../../../../constants'; import { ext } from '../../../../../../extensionVariables'; import { localize } from '../../../../../../localize'; @@ -89,7 +90,7 @@ export abstract class InitCodeProject extends AzureWizardExecuteStep { - const tasksJsonPath: string = path.join(context.projectPath, '.vscode', 'Tasks.json'); + const tasksJsonPath: string = path.join(context.projectPath, vscodeFolderName, 'Tasks.json'); const tasksJsonContent = `{ "version": "2.0.0", "tasks": [ diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/createNewCodeProject.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/createNewCodeProject.ts index 885fc177112..1f66c6d4311 100644 --- a/apps/vs-code-designer/src/app/commands/createNewCodeProject/createNewCodeProject.ts +++ b/apps/vs-code-designer/src/app/commands/createNewCodeProject/createNewCodeProject.ts @@ -2,31 +2,17 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { funcVersionSetting, projectLanguageSetting, projectOpenBehaviorSetting, projectTemplateKeySetting } from '../../../constants'; -import { localize } from '../../../localize'; -import { createArtifactsFolder } from '../../utils/codeless/artifacts'; -import { addLocalFuncTelemetry, tryGetLocalFuncVersion, tryParseFuncVersion } from '../../utils/funcCoreTools/funcVersion'; -import { getGlobalSetting, getWorkspaceSetting } from '../../utils/vsCodeConfig/settings'; +import { createNewProjectInternalBase } from './CodeProjectBase/CreateNewProjectInternal'; import { OpenBehaviorStep } from '../createNewProject/OpenBehaviorStep'; import { FolderListStep } from '../createNewProject/createProjectSteps/FolderListStep'; import { NewCodeProjectTypeStep } from './CodeProjectBase/NewCodeProjectTypeStep'; -import { OpenFolderStepCodeProject } from './CodeProjectBase/OpenFolderStepCodeProject'; +import { SetWorkspaceSettings } from './CodeProjectBase/SetWorkspaceSettings'; import { SetLogicAppName } from './CodeProjectBase/SetLogicAppNameStep'; -import { setWorkspaceName } from './CodeProjectBase/SetWorkspaceName'; +import { SetWorkspaceName } from './CodeProjectBase/SetWorkspaceName'; import { SetLogicAppType } from './CodeProjectBase/setLogicAppType'; import { isString } from '@microsoft/logic-apps-shared'; -import { AzureWizard } from '@microsoft/vscode-azext-utils'; import type { IActionContext } from '@microsoft/vscode-azext-utils'; -import { latestGAVersion, OpenBehavior, ProjectType } from '@microsoft/vscode-extension-logic-apps'; -import type { - ICreateFunctionOptions, - IFunctionWizardContext, - ProjectLanguage, - ProjectVersion, -} from '@microsoft/vscode-extension-logic-apps'; -import * as fse from 'fs-extra'; -import * as path from 'path'; -import { window } from 'vscode'; +import type { ProjectLanguage, ProjectVersion } from '@microsoft/vscode-extension-logic-apps'; import { TargetFrameworkStep } from './createCodeProjectSteps/createFunction/TargetFrameworkStep'; export async function createNewCodeProjectFromCommand( @@ -39,81 +25,28 @@ export async function createNewCodeProjectFromCommand( functionName?: string, functionSettings?: { [key: string]: string | undefined } ): Promise { - await createNewCodeProjectInternal(context, { - folderPath: isString(folderPath) ? folderPath : undefined, - templateId, - functionName, - functionSettings, - suppressOpenFolder: !openFolder, - language, - version, - }); -} - -export async function createNewCodeProjectInternal(context: IActionContext, options: ICreateFunctionOptions): Promise { - addLocalFuncTelemetry(context); - - const language: ProjectLanguage | string = (options.language as ProjectLanguage) || getGlobalSetting(projectLanguageSetting); - const version: string = options.version || getGlobalSetting(funcVersionSetting) || (await tryGetLocalFuncVersion()) || latestGAVersion; - const projectTemplateKey: string | undefined = getGlobalSetting(projectTemplateKeySetting); - const wizardContext: Partial & IActionContext = Object.assign(context, options, { - language, - version: tryParseFuncVersion(version), - projectTemplateKey, - }); - - if (options.folderPath) { - new FolderListStep.setProjectPath(wizardContext, options.folderPath); - } - - if (options.suppressOpenFolder) { - wizardContext.openBehavior = OpenBehavior.dontOpen; - } else if (!wizardContext.openBehavior) { - wizardContext.openBehavior = getWorkspaceSetting(projectOpenBehaviorSetting); - context.telemetry.properties.openBehaviorFromSetting = String(!!wizardContext.openBehavior); - } - - const wizard: AzureWizard = new AzureWizard(wizardContext, { - title: localize('createNewCodeProject', 'Create new logic app workspace'), - promptSteps: [ + await createNewProjectInternalBase( + context, + { + folderPath: isString(folderPath) ? folderPath : undefined, + templateId, + functionName, + functionSettings, + suppressOpenFolder: !openFolder, + language, + version, + }, + 'createNewCodeProject', + 'Create new logic app workspace', + [ new FolderListStep(), - new setWorkspaceName(), + new SetWorkspaceName(), new SetLogicAppType(), new TargetFrameworkStep(), new SetLogicAppName(), - new NewCodeProjectTypeStep(options.templateId, options.functionSettings, options.skipWorkflowStateTypeStep), + new NewCodeProjectTypeStep(templateId, functionSettings, false), + new SetWorkspaceSettings(), new OpenBehaviorStep(), - ], - executeSteps: [new OpenFolderStepCodeProject()], - }); - - await wizard.prompt(); - await wizard.execute(); - - await createArtifactsFolder(context as IFunctionWizardContext); - await createRulesFiles(context as IFunctionWizardContext); - await createLibFolder(context as IFunctionWizardContext); - - window.showInformationMessage(localize('finishedCreating', 'Finished creating project.')); -} - -/** - * Copies the rules files to the project directory if the project type is rulesEngine. - * @param {IFunctionWizardContext} context - The function wizard context. - * @returns A promise that resolves when the files are copied successfully. - */ -async function createRulesFiles(context: IFunctionWizardContext): Promise { - if (context.projectType === ProjectType.rulesEngine) { - const xmlTemplatePath = path.join(__dirname, 'assets', 'RuleSetProjectTemplate', 'SampleRuleSet'); - const xmlRuleSetPath = path.join(context.projectPath, 'Artifacts', 'Rules', 'SampleRuleSet.xml'); - const xmlTemplateContent = await fse.readFile(xmlTemplatePath, 'utf-8'); - const xmlFileContent = xmlTemplateContent.replace(/<%= methodName %>/g, context.methodName); - await fse.writeFile(xmlRuleSetPath, xmlFileContent); - } -} - -async function createLibFolder(context: IFunctionWizardContext): Promise { - fse.mkdirSync(path.join(context.projectPath, 'lib', 'builtinOperationSdks', 'JAR'), { recursive: true }); - fse.mkdirSync(path.join(context.projectPath, 'lib', 'builtinOperationSdks', 'net472'), { recursive: true }); - fse.mkdirSync(path.join(context.projectPath, 'lib', 'custom', 'net472'), { recursive: true }); + ] + ); } diff --git a/apps/vs-code-designer/src/app/commands/createNewProject/createNewProject.ts b/apps/vs-code-designer/src/app/commands/createNewProject/createNewProject.ts index 12a3c632cb7..42b53bcc9c7 100644 --- a/apps/vs-code-designer/src/app/commands/createNewProject/createNewProject.ts +++ b/apps/vs-code-designer/src/app/commands/createNewProject/createNewProject.ts @@ -2,28 +2,17 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { funcVersionSetting, projectLanguageSetting, projectOpenBehaviorSetting, projectTemplateKeySetting } from '../../../constants'; -import { localize } from '../../../localize'; -import { createArtifactsFolder } from '../../utils/codeless/artifacts'; -import { addLocalFuncTelemetry, tryGetLocalFuncVersion, tryParseFuncVersion } from '../../utils/funcCoreTools/funcVersion'; -import { getGlobalSetting, getWorkspaceSetting } from '../../utils/vsCodeConfig/settings'; -import { OpenBehaviorStep } from './OpenBehaviorStep'; -import { OpenFolderStep } from './OpenFolderStep'; -import { FolderListStep } from './createProjectSteps/FolderListStep'; -import { NewProjectTypeStep } from './createProjectSteps/NewProjectTypeStep'; +import { createNewProjectInternalBase } from '../createNewCodeProject/CodeProjectBase/CreateNewProjectInternal'; +import { ExistingWorkspaceStep } from './createProjectSteps/ExistingWorkspaceStep'; +import { SetLogicAppType } from '../createNewCodeProject/CodeProjectBase/setLogicAppType'; +import { SetLogicAppName } from '../createNewCodeProject/CodeProjectBase/SetLogicAppNameStep'; +import { TargetFrameworkStep } from '../createNewCodeProject/createCodeProjectSteps/createFunction/TargetFrameworkStep'; +import { NewCodeProjectTypeStep } from '../createNewCodeProject/CodeProjectBase/NewCodeProjectTypeStep'; +import { SetWorkspaceSettings } from '../createNewCodeProject/CodeProjectBase/SetWorkspaceSettings'; import { isString } from '@microsoft/logic-apps-shared'; -import { AzureWizard } from '@microsoft/vscode-azext-utils'; import type { IActionContext } from '@microsoft/vscode-azext-utils'; -import { latestGAVersion, OpenBehavior } from '@microsoft/vscode-extension-logic-apps'; -import type { - ICreateFunctionOptions, - IFunctionWizardContext, - ProjectLanguage, - ProjectVersion, -} from '@microsoft/vscode-extension-logic-apps'; -import * as fse from 'fs-extra'; -import * as path from 'path'; -import { window } from 'vscode'; +import type { ProjectLanguage, ProjectVersion } from '@microsoft/vscode-extension-logic-apps'; +import { ConvertToWorkspace } from '../createNewCodeProject/CodeProjectBase/ConvertToWorkspace'; export async function createNewProjectFromCommand( context: IActionContext, @@ -35,57 +24,28 @@ export async function createNewProjectFromCommand( functionName?: string, functionSettings?: { [key: string]: string | undefined } ): Promise { - await createNewProjectInternal(context, { - folderPath: isString(folderPath) ? folderPath : undefined, - templateId, - functionName, - functionSettings, - suppressOpenFolder: !openFolder, - language, - version, - }); -} - -export async function createNewProjectInternal(context: IActionContext, options: ICreateFunctionOptions): Promise { - addLocalFuncTelemetry(context); - - const language: ProjectLanguage | undefined = (options.language as ProjectLanguage) || getGlobalSetting(projectLanguageSetting); - const version: string = options.version || getGlobalSetting(funcVersionSetting) || (await tryGetLocalFuncVersion()) || latestGAVersion; - const projectTemplateKey: string | undefined = getGlobalSetting(projectTemplateKeySetting); - const wizardContext: Partial & IActionContext = Object.assign(context, options, { - language, - version: tryParseFuncVersion(version), - projectTemplateKey, - }); - - if (options.folderPath) { - FolderListStep.setProjectPath(wizardContext, options.folderPath); - } - - if (options.suppressOpenFolder) { - wizardContext.openBehavior = OpenBehavior.dontOpen; - } else if (!wizardContext.openBehavior) { - wizardContext.openBehavior = getWorkspaceSetting(projectOpenBehaviorSetting); - context.telemetry.properties.openBehaviorFromSetting = String(!!wizardContext.openBehavior); + if (await ConvertToWorkspace(context)) { + await createNewProjectInternalBase( + context, + { + folderPath: isString(folderPath) ? folderPath : undefined, + templateId, + functionName, + functionSettings, + suppressOpenFolder: !openFolder, + language, + version, + }, + 'createNewProject', + 'Create new project', + [ + new ExistingWorkspaceStep(), + new SetLogicAppType(), + new TargetFrameworkStep(), + new SetLogicAppName(), + new NewCodeProjectTypeStep(templateId, functionSettings, false), + new SetWorkspaceSettings(), + ] + ); } - - const wizard: AzureWizard = new AzureWizard(wizardContext, { - title: localize('createNewProject', 'Create new project'), - promptSteps: [new FolderListStep(), new NewProjectTypeStep(options.templateId, options.functionSettings), new OpenBehaviorStep()], - executeSteps: [new OpenFolderStep()], - }); - - await wizard.prompt(); - await wizard.execute(); - - await createArtifactsFolder(context as IFunctionWizardContext); - await createLibFolder(context as IFunctionWizardContext); - - window.showInformationMessage(localize('finishedCreating', 'Finished creating project.')); -} - -async function createLibFolder(context: IFunctionWizardContext): Promise { - fse.mkdirSync(path.join(context.projectPath, 'lib', 'builtinOperationSdks', 'JAR'), { recursive: true }); - fse.mkdirSync(path.join(context.projectPath, 'lib', 'builtinOperationSdks', 'net472'), { recursive: true }); - fse.mkdirSync(path.join(context.projectPath, 'lib', 'custom', 'net472'), { recursive: true }); } diff --git a/apps/vs-code-designer/src/app/commands/createNewProject/createProjectSteps/ExistingWorkspaceStep.ts b/apps/vs-code-designer/src/app/commands/createNewProject/createProjectSteps/ExistingWorkspaceStep.ts new file mode 100644 index 00000000000..0b021483a4e --- /dev/null +++ b/apps/vs-code-designer/src/app/commands/createNewProject/createProjectSteps/ExistingWorkspaceStep.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { getWorkspaceRoot, getWorkspaceFile } from '../../../utils/workspace'; +import { AzureWizardPromptStep } from '@microsoft/vscode-azext-utils'; +import * as fs from 'fs-extra'; +import type { IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps'; + +export class ExistingWorkspaceStep extends AzureWizardPromptStep { + public hideStepCount = true; + + public async prompt(context: IProjectWizardContext): Promise { + context.workspaceCustomFilePath = await getWorkspaceFile(context); + //save uri variable for open project folder command + context.customWorkspaceFolderPath = await getWorkspaceRoot(context); + context.workspacePath = context.customWorkspaceFolderPath; + await fs.ensureDir(context.workspacePath); + } + + public shouldPrompt(): boolean { + return true; + } +} diff --git a/apps/vs-code-designer/src/app/commands/createNewProject/createProjectSteps/ProjectCreateStepBase.ts b/apps/vs-code-designer/src/app/commands/createNewProject/createProjectSteps/ProjectCreateStepBase.ts index 9ff6fbc0da8..633ab9f5a2a 100644 --- a/apps/vs-code-designer/src/app/commands/createNewProject/createProjectSteps/ProjectCreateStepBase.ts +++ b/apps/vs-code-designer/src/app/commands/createNewProject/createProjectSteps/ProjectCreateStepBase.ts @@ -8,7 +8,6 @@ import { AzureWizardExecuteStep, callWithTelemetryAndErrorHandling } from '@micr import type { IActionContext } from '@microsoft/vscode-azext-utils'; import type { IProjectWizardContext } from '@microsoft/vscode-extension-logic-apps'; import * as fse from 'fs-extra'; -import * as path from 'path'; import type { Progress } from 'vscode'; export abstract class ProjectCreateStepBase extends AzureWizardExecuteStep { @@ -33,13 +32,7 @@ export abstract class ProjectCreateStepBase extends AzureWizardExecuteStep { try { @@ -48,6 +49,10 @@ export async function generateDeploymentScripts(context: IActionContext): Promis const connectionsData: ConnectionsData = isEmptyString(connectionsJson) ? {} : JSON.parse(connectionsJson); const isParameterized = await isConnectionsParameterized(connectionsData); const workflowFiles = getWorkflowFilePaths(projectPath); + if (!(await ConvertToWorkspace(context))) { + ext.outputChannel.appendLog(localize('exitScriptGen', 'Exiting script generation...')); + return; + } if (!isParameterized) { const message = localize( @@ -60,6 +65,7 @@ export async function generateDeploymentScripts(context: IActionContext): Promis context.telemetry.properties.parameterizeConnectionsInDeploymentScripts = 'true'; } else { context.telemetry.properties.parameterizeConnectionsInDeploymentScripts = 'false'; + ext.outputChannel.appendLog(localize('exitScriptGen', 'Exiting script generation...')); return; } } diff --git a/apps/vs-code-designer/src/app/commands/initProjectForVSCode/InitVSCodeStepBase.ts b/apps/vs-code-designer/src/app/commands/initProjectForVSCode/InitVSCodeStepBase.ts index 7898e37656a..d983ecda95d 100644 --- a/apps/vs-code-designer/src/app/commands/initProjectForVSCode/InitVSCodeStepBase.ts +++ b/apps/vs-code-designer/src/app/commands/initProjectForVSCode/InitVSCodeStepBase.ts @@ -81,7 +81,7 @@ export abstract class InitVSCodeStepBase extends AzureWizardExecuteStep 1) { - throw new Error(localize('multipleWorkspaces', 'Multiple workspaces are open.')); - } - target = workspaceFolders[0].uri; + if (target === undefined || Object.keys(target).length === 0) { + const workspaceFolder = await getWorkspaceFolder(context); + const projectPath = await tryGetLogicAppProjectRoot(context, workspaceFolder); + target = vscode.Uri.file(projectPath); } let version: FuncVersion | undefined = tryParseFuncVersion(getWorkspaceSetting(funcVersionSetting, target.fsPath)); @@ -99,12 +95,12 @@ export async function switchToDotnetProject(context: IProjectWizardContext, targ } if (!templates) { - throw new Error(localize('dotnetTemplateError', 'Dotnet templates could not be found.')); + throw new Error(localize('dotnetTemplateError', `Can't find dotnet templates.`)); } - + const logicAppFolderName = path.basename(target.fsPath); const warning: string = localize( 'confirmMoveToDotnet', - 'This action moves your Logic App project to a NuGet-based project. Confirm that you want to move to a NuGet-based project?' + `This action moves your logic app project, ${logicAppFolderName}, to a NuGet-based project. Confirm that you want to move to a NuGet-based project?` ); const moveButton: vscode.MessageItem = { title: localize('move', 'Move to a NuGet-based project') }; diff --git a/apps/vs-code-designer/src/app/utils/codeless/parameterizer.ts b/apps/vs-code-designer/src/app/utils/codeless/parameterizer.ts index a3af58344d8..8bb464ee0ac 100644 --- a/apps/vs-code-designer/src/app/utils/codeless/parameterizer.ts +++ b/apps/vs-code-designer/src/app/utils/codeless/parameterizer.ts @@ -65,6 +65,9 @@ export function parameterizeConnection( * @returns A boolean indicating whether the connections data is parameterized or not. */ export function isConnectionsParameterized(connectionsData: ConnectionsData): boolean { + if (!connectionsData || Object.keys(connectionsData).length === 0) { + return true; + } for (const connectionType in connectionsData) { if (connectionType !== 'serviceProviderConnections') { const connectionTypeJson = connectionsData[connectionType]; diff --git a/apps/vs-code-designer/src/app/utils/verifyIsProject.ts b/apps/vs-code-designer/src/app/utils/verifyIsProject.ts index cc42ac6b799..ba19169a057 100644 --- a/apps/vs-code-designer/src/app/utils/verifyIsProject.ts +++ b/apps/vs-code-designer/src/app/utils/verifyIsProject.ts @@ -21,6 +21,28 @@ export async function isLogicAppProject(folderPath: string): Promise { const hasHostJson: boolean = await fse.pathExists(hostFilePath); if (hasHostJson) { + const subpaths: string[] = await fse.readdir(folderPath); + const workflowJsonPaths = subpaths.map((subpath) => path.join(folderPath, subpath, 'workflow.json')); + const validWorkflowJsonPaths = await Promise.all( + workflowJsonPaths.map(async (workflowJsonPath) => { + if (await fse.pathExists(workflowJsonPath)) { + const workflowJsonData = await fse.readFile(workflowJsonPath, 'utf-8'); + const workflowJson = JSON.parse(workflowJsonData); + const schema = workflowJson?.definition?.$schema; + if (schema && schema.includes('Microsoft.Logic') && schema.includes('workflowdefinition.json')) { + const filesInSubpath = await fse.readdir(path.dirname(workflowJsonPath)); + if (filesInSubpath.length === 1 && filesInSubpath[0] === 'workflow.json') { + return true; + } + } + } + return false; + }) + ); + + if (!validWorkflowJsonPaths.some(Boolean)) { + return false; + } const hostJsonData = fse.readFileSync(hostFilePath, 'utf-8'); const hostJson = JSON.parse(hostJsonData); @@ -31,6 +53,39 @@ export async function isLogicAppProject(folderPath: string): Promise { return false; } +/** + * Checks root folder and subFolders one level down + * If any logic app projects are found return true. + */ +export async function isLogicAppProjectInRoot(workspaceFolder: WorkspaceFolder | string): Promise { + const subpath: string | undefined = getWorkspaceSetting(projectSubpathKey, workspaceFolder); + const folderPath = isString(workspaceFolder) ? workspaceFolder : workspaceFolder.uri.fsPath; + if (!subpath) { + if (!(await fse.pathExists(folderPath))) { + return undefined; + } + if (await isLogicAppProject(folderPath)) { + return true; + } + const subpaths: string[] = await fse.readdir(folderPath); + const matchingSubpaths: string[] = []; + await Promise.all( + subpaths.map(async (s) => { + if (await isLogicAppProject(path.join(folderPath, s))) { + matchingSubpaths.push(s); + } + }) + ); + + if (matchingSubpaths.length !== 0) { + return true; + } + return false; + } + + return false; +} + /** * Checks root folder and subFolders one level down * If a single logic app project is found, return that path. @@ -60,7 +115,7 @@ export async function tryGetLogicAppProjectRoot( }) ); - if (matchingSubpaths.length === 1) { + if (matchingSubpaths.length === 1 || (matchingSubpaths.length !== 0 && suppressPrompt)) { subpath = matchingSubpaths[0]; } else if (matchingSubpaths.length !== 0 && !suppressPrompt) { subpath = await promptForProjectSubpath(context, folderPath, matchingSubpaths); 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 8bd9a98194f..cd9e0724283 100644 --- a/apps/vs-code-designer/src/app/utils/vsCodeConfig/tasks.ts +++ b/apps/vs-code-designer/src/app/utils/vsCodeConfig/tasks.ts @@ -13,6 +13,7 @@ import * as fse from 'fs-extra'; import * as path from 'path'; import { workspace } from 'vscode'; import type { MessageItem, TaskDefinition, WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; +import { vscodeFolderName } from '../../../constants'; const tasksKey = 'tasks'; const inputsKey = 'inputs'; @@ -94,7 +95,7 @@ export async function validateTasksJson(context: IActionContext, folders: readon const projectPath: string | undefined = await tryGetLogicAppProjectRoot(context, folder); context.telemetry.properties.projectPath = projectPath; if (projectPath) { - const tasksJsonPath: string = path.join(projectPath, '.vscode', 'tasks.json'); + const tasksJsonPath: string = path.join(projectPath, vscodeFolderName, 'tasks.json'); if (!fse.existsSync(tasksJsonPath)) { throw new Error(localize('noTaskJson', `Failed to find: ${tasksJsonPath}`)); @@ -137,7 +138,7 @@ async function overwriteTasksJson(context: IActionContext, projectPath: string): '\n\nSelecting "Cancel" leaves the file unchanged, but shows this message when you open this project again.' + '\n\nContinue with the update?'; - const tasksJsonPath: string = path.join(projectPath, '.vscode', 'tasks.json'); + const tasksJsonPath: string = path.join(projectPath, vscodeFolderName, 'tasks.json'); let tasksJsonContent: any; const projectFiles = [ diff --git a/apps/vs-code-designer/src/app/utils/workspace.ts b/apps/vs-code-designer/src/app/utils/workspace.ts index 68d3c7f9d82..15135bbcd3a 100644 --- a/apps/vs-code-designer/src/app/utils/workspace.ts +++ b/apps/vs-code-designer/src/app/utils/workspace.ts @@ -14,6 +14,75 @@ import globby from 'globby'; import * as path from 'path'; import * as vscode from 'vscode'; +/** + * Gets the folder path that contains the .code-workspace file. + * @param {IActionContext} actionContext - The action context. + * @returns A promise that resolves to a string of the folder path that contains the .code-workspace file. + */ +export const getWorkspaceRoot = async (actionContext: IActionContext): Promise => { + if (vscode.workspace.workspaceFolders !== undefined) { + for (const folder of vscode.workspace.workspaceFolders) { + const projectRoot = await tryGetLogicAppProjectRoot(actionContext, folder, true); + if (projectRoot) { + return vscode.workspace.workspaceFile ? path.dirname(vscode.workspace.workspaceFile.fsPath) : undefined; + } + } + } + return undefined; +}; + +/** + * Gets the workspace file path. + * @param {IActionContext} actionContext - The action context. + * @returns A promise that resolves to a string of the .code-workspace file path. + */ +export const getWorkspaceFile = async (actionContext: IActionContext): Promise => { + if (vscode.workspace.workspaceFolders !== undefined) { + for (const folder of vscode.workspace.workspaceFolders) { + const projectRoot = await tryGetLogicAppProjectRoot(actionContext, folder, true); + if (projectRoot) { + return vscode.workspace.workspaceFile ? vscode.workspace.workspaceFile.fsPath : undefined; + } + } + } + return undefined; +}; + +/** + * Gets the workspace file within the current directory or parent directory. + * @param {IActionContext} actionContext - The action context. + * @returns A promise that resolves to a string of the .code-workspace file path. + */ +export const getWorkspaceFileInParentDirectory = async (actionContext: IActionContext): Promise => { + if (vscode.workspace.workspaceFolders !== undefined) { + for (const folder of vscode.workspace.workspaceFolders) { + const projectRoot = await tryGetLogicAppProjectRoot(actionContext, folder, true); + if (projectRoot) { + if (vscode.workspace.workspaceFile) { + return vscode.workspace.workspaceFile.fsPath; + } + const parentDir = path.dirname(projectRoot); + const currentFolder = path.basename(projectRoot); + const relativeFolderPath = `./${currentFolder}`; + const workspaceFiles = await globby('*.code-workspace', { cwd: parentDir }); + if (workspaceFiles.length > 0) { + const workspaceFilePath = path.join(parentDir, workspaceFiles[0]); + const workspaceFileContent = await vscode.workspace.fs.readFile(vscode.Uri.file(workspaceFilePath)); + const workspaceFileJson = JSON.parse(workspaceFileContent.toString()); + + if ( + workspaceFileJson.folders && + workspaceFileJson.folders.some((folder: { path: string }) => folder.path === relativeFolderPath) + ) { + return workspaceFilePath; + } + } + } + } + } + return undefined; +}; + /** * Gets workspace folder from the workflow file path. * @param {string} fsPath - Workflow file path. @@ -61,7 +130,17 @@ export async function getWorkspaceFolder( folder = logicAppsWorkspaces[0]; } else { const placeHolder: string = localize('selectProjectFolder', 'Select the folder containing your logic app project'); - folder = await vscode.window.showWorkspaceFolderPick({ placeHolder }); + const folderPicks: IAzureQuickPickItem[] = logicAppsWorkspaces.map((projectRoot) => { + const workspaceFolder = vscode.workspace.workspaceFolders?.find((folder) => folder.uri.fsPath === projectRoot); + return { + label: path.basename(projectRoot), + description: projectRoot, + data: workspaceFolder, + }; + }); + + const selectedItem = await context.ui.showQuickPick(folderPicks, { placeHolder }); + folder = selectedItem?.data; if (!folder) { throw new UserCancelledError(); } diff --git a/apps/vs-code-designer/src/main.ts b/apps/vs-code-designer/src/main.ts index 0cfbc8e000b..38a2c4de94c 100644 --- a/apps/vs-code-designer/src/main.ts +++ b/apps/vs-code-designer/src/main.ts @@ -31,6 +31,7 @@ import { } from '@microsoft/vscode-azext-utils'; import type { IActionContext } from '@microsoft/vscode-azext-utils'; import * as vscode from 'vscode'; +import { ConvertToWorkspace } from './app/commands/createNewCodeProject/CodeProjectBase/ConvertToWorkspace'; import TelemetryReporter from '@vscode/extension-telemetry'; const perfStats = { @@ -69,6 +70,10 @@ export async function activate(context: vscode.ExtensionContext) { runPostWorkflowCreateStepsFromCache(); runPostExtractStepsFromCache(); + if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { + await ConvertToWorkspace(activateContext); + } + try { await downloadExtensionBundle(activateContext); } catch (error) { diff --git a/apps/vs-code-designer/src/package.json b/apps/vs-code-designer/src/package.json index e013ebb5082..40be5e6594d 100644 --- a/apps/vs-code-designer/src/package.json +++ b/apps/vs-code-designer/src/package.json @@ -375,18 +375,10 @@ ], "menus": { "azureLogicAppsStandard.submenus.workspaceActions": [ - { - "command": "azureLogicAppsStandard.createNewProject", - "group": "1_create@1" - }, { "command": "azureLogicAppsStandard.createNewCodeProject", "group": "1_create@2" }, - { - "command": "azureLogicAppsStandard.createCodeless", - "group": "1_create@3" - }, { "command": "azureLogicAppsStandard.deploy", "group": "2_deploy@1" @@ -650,6 +642,16 @@ "when": "explorerResourceIsRoot == true", "group": "zzz_LogicApptools@1" }, + { + "command": "azureLogicAppsStandard.createNewProject", + "when": "explorerResourceIsRoot == true", + "group": "zzz_LogicApptools@1" + }, + { + "command": "azureLogicAppsStandard.createCodeless", + "when": "explorerResourceIsRoot == true", + "group": "zzz_LogicApptools@1" + }, { "command": "azureLogicAppsStandard.openOverview", "when": "resourceFilename==workflow.json", @@ -728,6 +730,10 @@ { "command": "azureLogicAppsStandard.generateDeploymentScripts", "when": "never" + }, + { + "command": "azureLogicAppsStandard.createNewProject", + "when": "never" } ] }, diff --git a/libs/vscode-extension/src/lib/models/functions.ts b/libs/vscode-extension/src/lib/models/functions.ts index d30601d7647..271814b03e4 100644 --- a/libs/vscode-extension/src/lib/models/functions.ts +++ b/libs/vscode-extension/src/lib/models/functions.ts @@ -92,10 +92,6 @@ export interface ICreateFunctionOptions { * If set to true, it will not try to open the folder after create finishes. Defaults to false */ suppressOpenFolder?: boolean; - /** - * If set to true, it will not prompt for the workflow state type step. Defaults to false - */ - skipWorkflowStateTypeStep?: boolean; } export interface INpmDistTag {