diff --git a/apps/vs-code-designer/src/app/commands/__test__/publishCodefulProject.test.ts b/apps/vs-code-designer/src/app/commands/__test__/publishCodefulProject.test.ts index 34dcc2331c7..ffcea613580 100644 --- a/apps/vs-code-designer/src/app/commands/__test__/publishCodefulProject.test.ts +++ b/apps/vs-code-designer/src/app/commands/__test__/publishCodefulProject.test.ts @@ -17,10 +17,9 @@ vi.mock('../../utils/workspace', () => ({ vi.mock('../../utils/codeful', () => ({ isCodefulProject: vi.fn(), inspectCodefulCsprojBuildHooks: vi.fn(), - invalidateCodefulSdkCacheIfNeeded: vi.fn(), })); -import { inspectCodefulCsprojBuildHooks, invalidateCodefulSdkCacheIfNeeded } from '../../utils/codeful'; +import { inspectCodefulCsprojBuildHooks } from '../../utils/codeful'; describe('publishCodefulProject', () => { const projectPath = 'D:\\workspace\\CodefulLogicApp'; @@ -45,7 +44,6 @@ describe('publishCodefulProject', () => { }; (getWorkspaceRoot as Mock).mockResolvedValue(projectPath); (isCodefulProject as Mock).mockResolvedValue(true); - (invalidateCodefulSdkCacheIfNeeded as Mock).mockResolvedValue(false); (inspectCodefulCsprojBuildHooks as Mock).mockResolvedValue({ copyAfterTargets: 'Build;Publish', replaceLangAfterTargets: 'Build;Publish', @@ -96,7 +94,6 @@ describe('publishCodefulProject', () => { await publishCodefulProject(context, { fsPath: projectPath } as vscode.Uri); expect((vscode as any).tasks.executeTask).toHaveBeenCalledWith(publishTask); - expect(invalidateCodefulSdkCacheIfNeeded).toHaveBeenCalledWith(projectPath); expect(dispose).toHaveBeenCalled(); expect(ext.outputChannel.appendLog).toHaveBeenCalledWith(`Codeful project published successfully at ${projectPath}.`); expect(context.telemetry.properties.result).toBe('Succeeded'); diff --git a/apps/vs-code-designer/src/app/commands/binaries/__test__/validateAndInstallBinaries.test.ts b/apps/vs-code-designer/src/app/commands/binaries/__test__/validateAndInstallBinaries.test.ts index 4d2ae5c12c4..f7d5767bde2 100644 --- a/apps/vs-code-designer/src/app/commands/binaries/__test__/validateAndInstallBinaries.test.ts +++ b/apps/vs-code-designer/src/app/commands/binaries/__test__/validateAndInstallBinaries.test.ts @@ -6,7 +6,7 @@ import { getDependencyTimeout } from '../../../utils/binaries'; import { getDependenciesVersion } from '../../../utils/bundleFeed'; import { setDotNetCommand } from '../../../utils/dotnet/dotnet'; import { setFunctionsCommand } from '../../../utils/funcCoreTools/funcVersion'; -import { installLSPSDK } from '../../../utils/languageServerProtocol'; +import { installLSPServer } from '../../../utils/languageServerProtocol'; import { setNodeJsCommand } from '../../../utils/nodeJs/nodeJsVersion'; import { runWithDurationTelemetry } from '../../../utils/telemetry'; import { timeout } from '../../../utils/timeout'; @@ -42,7 +42,7 @@ vi.mock('../../../utils/funcCoreTools/funcVersion', () => ({ })); vi.mock('../../../utils/languageServerProtocol', () => ({ - installLSPSDK: vi.fn(), + installLSPServer: vi.fn(), })); vi.mock('../../../utils/nodeJs/nodeJsVersion', () => ({ @@ -102,7 +102,7 @@ describe('validateAndInstallBinaries', () => { (validateNodeJsIsLatest as Mock).mockResolvedValue(undefined); (validateFuncCoreToolsIsLatest as Mock).mockResolvedValue(undefined); (validateDotNetIsLatest as Mock).mockResolvedValue(undefined); - (installLSPSDK as Mock).mockResolvedValue(undefined); + (installLSPServer as Mock).mockResolvedValue(undefined); (setNodeJsCommand as Mock).mockResolvedValue(undefined); (setFunctionsCommand as Mock).mockResolvedValue(undefined); (setDotNetCommand as Mock).mockResolvedValue(undefined); @@ -141,7 +141,7 @@ describe('validateAndInstallBinaries', () => { expect(timeout).toHaveBeenCalledWith(validateDotNetIsLatest, '.NET SDK', 3000, 'https://dotnet.microsoft.com/en-us/download/dotnet', [ '8.0.100', ]); - expect(timeout).toHaveBeenCalledWith(installLSPSDK, 'LSP SDK', 3000); + expect(timeout).toHaveBeenCalledWith(installLSPServer, 'LSP Server', 3000); expect(setNodeJsCommand).toHaveBeenCalled(); expect(setFunctionsCommand).toHaveBeenCalled(); expect(setDotNetCommand).toHaveBeenCalledTimes(2); diff --git a/apps/vs-code-designer/src/app/commands/binaries/validateAndInstallBinaries.ts b/apps/vs-code-designer/src/app/commands/binaries/validateAndInstallBinaries.ts index 68511af04ad..fb13fe016cc 100644 --- a/apps/vs-code-designer/src/app/commands/binaries/validateAndInstallBinaries.ts +++ b/apps/vs-code-designer/src/app/commands/binaries/validateAndInstallBinaries.ts @@ -9,7 +9,7 @@ import { getDependenciesVersion } from '../../utils/bundleFeed'; import { setDotNetCommand } from '../../utils/dotnet/dotnet'; import { executeCommand } from '../../utils/funcCoreTools/cpUtils'; import { setFunctionsCommand } from '../../utils/funcCoreTools/funcVersion'; -import { installLSPSDK } from '../../utils/languageServerProtocol'; +import { installLSPServer } from '../../utils/languageServerProtocol'; import { setNodeJsCommand } from '../../utils/nodeJs/nodeJsVersion'; import { ensureRuntimeDependenciesPath } from '../../utils/runtimeDependenciesPath'; import { runWithDurationTelemetry } from '../../utils/telemetry'; @@ -97,10 +97,10 @@ export async function validateAndInstallBinaries(context: IActionContext) { await setDotNetCommand(); }); - context.telemetry.properties.lastStep = 'installLSPSDK'; - await runWithDurationTelemetry(context, 'azureLogicAppsStandard.installLSPSDK', async () => { - progress.report({ increment: 10, message: 'LSP SDK' }); - await timeout(installLSPSDK, 'LSP SDK', dependencyTimeout); + context.telemetry.properties.lastStep = 'installLSPServer'; + await runWithDurationTelemetry(context, 'azureLogicAppsStandard.installLSPServer', async () => { + progress.report({ increment: 10, message: 'LSP Server' }); + await timeout(installLSPServer, 'LSP Server', dependencyTimeout); await setDotNetCommand(); }); diff --git a/apps/vs-code-designer/src/app/commands/buildCustomCodeFunctionsProject.ts b/apps/vs-code-designer/src/app/commands/buildCustomCodeFunctionsProject.ts index 6fb40068337..6190dae0e2c 100644 --- a/apps/vs-code-designer/src/app/commands/buildCustomCodeFunctionsProject.ts +++ b/apps/vs-code-designer/src/app/commands/buildCustomCodeFunctionsProject.ts @@ -13,7 +13,6 @@ import { } from '../utils/customCodeUtils'; import * as vscode from 'vscode'; import { isNullOrUndefined } from '@microsoft/logic-apps-shared'; -import { invalidateCodefulSdkCacheIfNeeded } from '../utils/codeful'; /** * Builds a custom code functions project if exists. @@ -92,8 +91,6 @@ export async function buildWorkspaceCustomCodeFunctionsProjects(context: IAction } async function buildCustomCodeProject(functionsProjectPath: string): Promise { - await invalidateCodefulSdkCacheIfNeeded(functionsProjectPath); - const tasks: vscode.Task[] = await vscode.tasks.fetchTasks(); const buildTask = tasks.find((task) => { const currTaskPath = (task.scope as vscode.WorkspaceFolder)?.uri.fsPath; diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/CreateLogicAppWorkspace.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/CreateLogicAppWorkspace.ts index 8e4d1bf1b7d..337b2cd00fa 100644 --- a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/CreateLogicAppWorkspace.ts +++ b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/CreateLogicAppWorkspace.ts @@ -2,7 +2,6 @@ import { appKindSetting, artifactsDirectory, assetsFolderName, - autoRuntimeDependenciesPathSettingKey, azureWebJobsFeatureFlagsKey, azureWebJobsStorageKey, defaultVersionRange, @@ -18,7 +17,6 @@ import { localEmulatorConnectionString, localSettingsFileName, logicAppKind, - lspDirectory, multiLanguageWorkerSetting, ProjectDirectoryPathKey, rulesDirectory, @@ -54,7 +52,6 @@ import { WorkerRuntime, ProjectType, WorkflowType } from '@microsoft/vscode-exte import { createDevContainerContents, createLogicAppVsCodeContents } from './CreateLogicAppVSCodeContents'; import { logicAppPackageProcessing, unzipLogicAppPackageIntoWorkspace } from '../../../utils/cloudToLocalUtils'; import { isLogicAppProject } from '../../../utils/verifyIsProject'; -import { getGlobalSetting } from '../../../utils/vsCodeConfig/settings'; export async function createRulesFiles(context: IFunctionWizardContext): Promise { if (context.projectType === ProjectType.rulesEngine) { @@ -156,8 +153,6 @@ export const createCodefulWorkflowFile = async ( workflowType: WorkflowType ) => { const workflowTemplateFileName = getCodefulWorkflowTemplateFileName(workflowType); - const targetDirectory = getGlobalSetting(autoRuntimeDependenciesPathSettingKey); - const lspDirectoryPath = path.join(targetDirectory, lspDirectory); // Create the workflow-specific .cs file const capitalizedWorkflowName = (workflowName.charAt(0).toUpperCase() + workflowName.slice(1)).replace(/-/g, '_'); @@ -180,8 +175,7 @@ export const createCodefulWorkflowFile = async ( const templateProgramPath = path.join(__dirname, assetsFolderName, 'CodefulProjectTemplate', 'ProgramFile'); const templateProgramContent = await fse.readFile(templateProgramPath, 'utf-8'); const programContent = templateProgramContent - .replace(/<%= logicAppNamespace %>/g, `${logicAppName}`) - .replace(/<%= workflowBuilders %>/g, ''); + .replace(/<%= logicAppNamespace %>/g, `${logicAppName}`); await fse.writeFile(programFilePath, programContent); // Create the .csproj file (only for first workflow) @@ -189,12 +183,6 @@ export const createCodefulWorkflowFile = async ( const templateProjContent = await fse.readFile(templateProjPath, 'utf-8'); const csprojFilePath = path.join(logicAppFolderPath, `${logicAppName}.csproj`); await fse.writeFile(csprojFilePath, templateProjContent); - - // Create nuget.config file (only for first workflow) - const templateNugetPath = path.join(__dirname, assetsFolderName, 'CodefulProjectTemplate', 'nuget'); - const templateNugetContent = (await fse.readFile(templateNugetPath, 'utf-8')).replace(/<%= lspDirectory %>/g, `"${lspDirectoryPath}"`); - const nugetFilePath = path.join(logicAppFolderPath, 'nuget.config'); - await fse.writeFile(nugetFilePath, templateNugetContent); } }; diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/__test__/CreateLogicAppWorkspace.test.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/__test__/CreateLogicAppWorkspace.test.ts index 18f913f9896..7f921693d14 100644 --- a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/__test__/CreateLogicAppWorkspace.test.ts +++ b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/__test__/CreateLogicAppWorkspace.test.ts @@ -77,7 +77,6 @@ describe('CreateLogicAppWorkspace - Codeful Workflows', () => { const testProjectPath = '/test/project'; const testProjectName = 'TestProject'; const testWorkflowName = 'TestWorkflow'; - const testLspDirectory = '/test/lsp'; describe('StatefulCodefulWorkflow template content', () => { it('should avoid unsupported member-expression startup patterns while keeping MSN Weather', () => { @@ -132,18 +131,6 @@ describe('CreateLogicAppWorkspace - Codeful Workflows', () => { }); }); - describe('nuget template content', () => { - it('should use a project-local package cache for the codeful SDK package', () => { - const templateContent = actualFs.readFileSync( - new URL('../../../../../assets/CodefulProjectTemplate/nuget', import.meta.url), - 'utf-8' - ); - - expect(templateContent).toContain(''); - expect(templateContent).toContain(' />'); - }); - }); - beforeEach(() => { // Reset all mocks vi.clearAllMocks(); @@ -151,9 +138,6 @@ describe('CreateLogicAppWorkspace - Codeful Workflows', () => { // Restore path.join to use actual implementation vi.mocked(path.join).mockImplementation((...args: string[]) => actualPath.join(...args)); vi.mocked(path.resolve).mockImplementation((...args: string[]) => actualPath.resolve(...args)); - - // Default getGlobalSetting behavior - vi.mocked(vscodeConfigModule.getGlobalSetting).mockReturnValue(testLspDirectory); }); afterEach(() => { @@ -246,7 +230,7 @@ describe('CreateLogicAppWorkspace - Codeful Workflows', () => { it('should create stateful provider workflow without adding a Program.cs AddWorkflow call', async () => { const statefulTemplate = 'namespace <%= logicAppNamespace %>\npublic static class <%= flowNameClass %> { stateful content }'; - const programTemplate = 'namespace <%= logicAppNamespace %>\nclass Program { <%= workflowBuilders %> }'; + const programTemplate = 'namespace <%= logicAppNamespace %>\nclass Program { }'; vi.mocked(fse.readFile).mockImplementation((filePath: string) => { if (filePath.includes('StatefulCodefulWorkflow')) { @@ -311,7 +295,7 @@ describe('CreateLogicAppWorkspace - Codeful Workflows', () => { it('should create workflow file with workflow name (not Program.cs) for first workflow', async () => { const agentCodefulTemplate = 'namespace <%= logicAppNamespace %>\npublic static class <%= flowNameClass %> { }'; - const programTemplate = 'namespace <%= logicAppNamespace %>\nclass Program { <%= workflowBuilders %> }'; + const programTemplate = 'namespace <%= logicAppNamespace %>\nclass Program { }'; vi.mocked(fse.readFile).mockImplementation((filePath: string) => { if (filePath.includes('AgentCodefulWorkflow')) { @@ -452,7 +436,7 @@ describe('CreateLogicAppWorkspace - Codeful Workflows', () => { it('should create StatefulCodeful workflow file correctly with namespace', async () => { const statefulTemplate = 'namespace <%= logicAppNamespace %>\npublic static class <%= flowNameClass %> { stateful content }'; - const programTemplate = 'namespace <%= logicAppNamespace %>\nclass Program { <%= workflowBuilders %> }'; + const programTemplate = 'namespace <%= logicAppNamespace %>\nclass Program { }'; vi.mocked(fse.readFile).mockImplementation((filePath: string) => { if (filePath.includes('StatefulCodefulWorkflow')) { @@ -491,11 +475,10 @@ describe('CreateLogicAppWorkspace - Codeful Workflows', () => { } }); - it('should create .csproj and nuget.config files', async () => { + it('should create .csproj file', async () => { const agentCodefulTemplate = 'public static class <%= flowName %> { }'; - const programTemplate = 'class Program { <%= workflowBuilders %> }'; + const programTemplate = 'class Program { }'; const projTemplate = 'test proj'; - const nugetTemplate = '<%= lspDirectory %>'; vi.mocked(fse.readFile).mockImplementation((filePath: string) => { if (filePath.includes('AgentCodefulWorkflow')) { @@ -507,9 +490,6 @@ describe('CreateLogicAppWorkspace - Codeful Workflows', () => { if (filePath.includes('CodefulProj')) { return Promise.resolve(projTemplate); } - if (filePath.includes('nuget')) { - return Promise.resolve(nugetTemplate); - } return Promise.reject(new Error('Unexpected file read')); }); @@ -527,10 +507,9 @@ describe('CreateLogicAppWorkspace - Codeful Workflows', () => { expect.stringContaining('test proj') ); - // Verify nuget.config was created + // Verify nuget.config was NOT created (SDK is on nuget.org by default) const nugetCall = vi.mocked(fse.writeFile).mock.calls.find((call: any) => call[0].includes('nuget.config')); - expect(nugetCall).toBeDefined(); - expect(nugetCall[0]).toContain('nuget.config'); + expect(nugetCall).toBeUndefined(); } }); }); diff --git a/apps/vs-code-designer/src/app/commands/createWorkflow/createCodefulWorkflow/createCodefulWorkflowSteps/__test__/codefulWorkflowCreateStep.test.ts b/apps/vs-code-designer/src/app/commands/createWorkflow/createCodefulWorkflow/createCodefulWorkflowSteps/__test__/codefulWorkflowCreateStep.test.ts index 7aa19cb301b..c80076bbc47 100644 --- a/apps/vs-code-designer/src/app/commands/createWorkflow/createCodefulWorkflow/createCodefulWorkflowSteps/__test__/codefulWorkflowCreateStep.test.ts +++ b/apps/vs-code-designer/src/app/commands/createWorkflow/createCodefulWorkflow/createCodefulWorkflowSteps/__test__/codefulWorkflowCreateStep.test.ts @@ -2,9 +2,6 @@ import { beforeEach, describe, it, expect, vi, Mock } from 'vitest'; import { CodefulWorkflowCreateStep } from '../codefulWorkflowCreateStep'; import { IFunctionWizardContext, WorkerRuntime } from '@microsoft/vscode-extension-logic-apps'; import { setLocalAppSetting } from '../../../../../utils/appSettings/localSettings'; -import { getGlobalSetting } from '../../../../../utils/vsCodeConfig/settings'; -import * as fse from 'fs-extra'; -import path from 'path'; import { appKindSetting, azureWebJobsStorageKey, @@ -15,10 +12,6 @@ import { workerRuntimeKey, } from '../../../../../../constants'; -vi.mock('../../../../../utils/vsCodeConfig/settings', () => ({ - getGlobalSetting: vi.fn(), -})); - describe('CodefulWorkflowCreateStep', async () => { beforeEach(() => { vi.clearAllMocks(); @@ -57,58 +50,4 @@ describe('CodefulWorkflowCreateStep', async () => { ); }); }); - - describe('addNugetConfig', () => { - const projectPath = 'D:\\logicapp'; - const dependenciesPath = 'D:\\runtime-dependencies'; - const templateNugetConfig = ` - - - - - - /> - -`; - - it('writes the shared codeful NuGet config when nuget.config is missing', async () => { - const testCodefulWorkflowCreateStep = new CodefulWorkflowCreateStep(); - vi.mocked(getGlobalSetting).mockReturnValue(dependenciesPath); - vi.mocked(fse.pathExists).mockResolvedValue(false); - vi.mocked(fse.readFile).mockResolvedValue(templateNugetConfig); - - await (testCodefulWorkflowCreateStep as any).addNugetConfig(projectPath); - - const writeCall = vi.mocked(fse.writeFile).mock.calls.find((call) => String(call[0]).endsWith('nuget.config')); - expect(writeCall).toBeDefined(); - expect(writeCall?.[0]).toBe(path.join(projectPath, 'nuget.config')); - expect(writeCall?.[1]).toContain(''); - expect(writeCall?.[1]).toContain(``); - expect(writeCall?.[1]).not.toContain('C:\\dev\\.packages'); - }); - - it('merges the local SDK package source into an existing nuget.config without removing user sources', async () => { - const testCodefulWorkflowCreateStep = new CodefulWorkflowCreateStep(); - const existingNugetConfig = ` - - - - -`; - vi.mocked(getGlobalSetting).mockReturnValue(dependenciesPath); - vi.mocked(fse.pathExists).mockResolvedValue(true); - vi.mocked(fse.readFile).mockImplementation(async (filePath: string) => { - return String(filePath).includes('CodefulProjectTemplate') ? templateNugetConfig : existingNugetConfig; - }); - - await (testCodefulWorkflowCreateStep as any).addNugetConfig(projectPath); - - const writeCall = vi.mocked(fse.writeFile).mock.calls.find((call) => String(call[0]).endsWith('nuget.config')); - expect(writeCall).toBeDefined(); - expect(writeCall?.[1]).toContain(''); - expect(writeCall?.[1]).toContain(''); - expect(writeCall?.[1]).toContain(``); - expect(writeCall?.[1]).not.toContain('C:\\dev\\.packages'); - }); - }); }); diff --git a/apps/vs-code-designer/src/app/commands/createWorkflow/createCodefulWorkflow/createCodefulWorkflowSteps/codefulWorkflowCreateStep.ts b/apps/vs-code-designer/src/app/commands/createWorkflow/createCodefulWorkflow/createCodefulWorkflowSteps/codefulWorkflowCreateStep.ts index 3011c214cf3..bd1d5d807d1 100644 --- a/apps/vs-code-designer/src/app/commands/createWorkflow/createCodefulWorkflow/createCodefulWorkflowSteps/codefulWorkflowCreateStep.ts +++ b/apps/vs-code-designer/src/app/commands/createWorkflow/createCodefulWorkflow/createCodefulWorkflowSteps/codefulWorkflowCreateStep.ts @@ -25,9 +25,6 @@ import { extensionCommand, launchFileName, vscodeFolderName, - assetsFolderName, - autoRuntimeDependenciesPathSettingKey, - lspDirectory, } from '../../../../../constants'; import { removeAppKindFromLocalSettings, setLocalAppSetting } from '../../../../utils/appSettings/localSettings'; import { validateDotnetInstalled } from '../../../../utils/dotnet/executeDotnetTemplateCommand'; @@ -38,7 +35,6 @@ import { createEmptyParametersJson } from '../../../../utils/codeless/parameter' import { getDebugConfigs, updateDebugConfigs } from '../../../../utils/vsCodeConfig/launch'; import { getWorkspaceFolder, isMultiRootWorkspace } from '../../../../utils/workspace'; import { getDebugConfiguration } from '../../../../utils/debug'; -import { getGlobalSetting } from '../../../../utils/vsCodeConfig/settings'; export class CodefulWorkflowCreateStep extends WorkflowCreateStepBase { public async executeCore(context: IFunctionWizardContext): Promise { @@ -54,7 +50,6 @@ export class CodefulWorkflowCreateStep extends WorkflowCreateStepBase { - const targetDirectory = getGlobalSetting(autoRuntimeDependenciesPathSettingKey); - const lspDirectoryPath = path.join(targetDirectory, lspDirectory); - const templateNugetPath = path.join(__dirname, assetsFolderName, 'CodefulProjectTemplate', 'nuget'); - const getNugetConfigTemplate = (await fse.readFile(templateNugetPath, 'utf-8')).replace( - /<%= lspDirectory %>/g, - `"${lspDirectoryPath}"` - ); - const nugetConfigPath: string = path.join(projectPath, 'nuget.config'); - - if (!(await fse.pathExists(nugetConfigPath))) { - await fse.writeFile(nugetConfigPath, getNugetConfigTemplate); - return; - } - - const existingNugetConfig = await fse.readFile(nugetConfigPath, 'utf-8'); - const updatedNugetConfig = this.mergeCodefulNugetConfig(existingNugetConfig, lspDirectoryPath); - if (updatedNugetConfig !== existingNugetConfig) { - await fse.writeFile(nugetConfigPath, updatedNugetConfig); - } - } - - private mergeCodefulNugetConfig(nugetConfig: string, lspDirectoryPath: string): string { - let updatedNugetConfig = this.upsertXmlAddElement( - nugetConfig, - 'config', - 'globalPackagesFolder', - '.nuget\\packages', - '\n ' - ); - - updatedNugetConfig = this.upsertXmlAddElement( - updatedNugetConfig, - 'packageSources', - 'current', - lspDirectoryPath, - '\n ' - ); - - return updatedNugetConfig; - } - - private upsertXmlAddElement(nugetConfig: string, sectionName: string, key: string, value: string, emptySection: string): string { - const addLine = ` `; - const addElementRegex = new RegExp(``); - if (addElementRegex.test(nugetConfig)) { - return nugetConfig.replace(addElementRegex, addLine.trim()); - } - - const sectionCloseTag = ``; - if (nugetConfig.includes(sectionCloseTag)) { - return nugetConfig.replace(sectionCloseTag, `${addLine}\n ${sectionCloseTag}`); - } - - const configurationCloseTag = ''; - const sectionWithValue = emptySection.replace(``, `${addLine}\n `); - return nugetConfig.replace(configurationCloseTag, ` ${sectionWithValue}\n${configurationCloseTag}`); - } } diff --git a/apps/vs-code-designer/src/app/commands/publishCodefulProject.ts b/apps/vs-code-designer/src/app/commands/publishCodefulProject.ts index 17d01541559..3670f86f201 100644 --- a/apps/vs-code-designer/src/app/commands/publishCodefulProject.ts +++ b/apps/vs-code-designer/src/app/commands/publishCodefulProject.ts @@ -8,7 +8,7 @@ import { ext } from '../../extensionVariables'; import { getWorkspaceRoot } from '../utils/workspace'; import * as vscode from 'vscode'; import { isNullOrUndefined } from '@microsoft/logic-apps-shared'; -import { inspectCodefulCsprojBuildHooks, invalidateCodefulSdkCacheIfNeeded, isCodefulProject } from '../utils/codeful'; +import { inspectCodefulCsprojBuildHooks, isCodefulProject } from '../utils/codeful'; /** * Optional behaviors for {@link publishCodefulProject}. @@ -59,8 +59,6 @@ export async function publishCodefulProject( return; } - await invalidateCodefulSdkCacheIfNeeded(nodePath); - if (options?.skipIfBuildPopulatesCodeful) { const buildHooks = await inspectCodefulCsprojBuildHooks(nodePath); if (buildHooks) { diff --git a/apps/vs-code-designer/src/app/languageServer/__test__/languageServer.test.ts b/apps/vs-code-designer/src/app/languageServer/__test__/languageServer.test.ts index 06f2a763db0..7b783e857d0 100644 --- a/apps/vs-code-designer/src/app/languageServer/__test__/languageServer.test.ts +++ b/apps/vs-code-designer/src/app/languageServer/__test__/languageServer.test.ts @@ -14,6 +14,7 @@ const mocks = vi.hoisted(() => ({ showWarningMessage: vi.fn(), tryGetLogicAppProjectRoot: vi.fn(), getDotNetCommand: vi.fn(), + resolveSdkFromProject: vi.fn(), })); vi.mock('vscode', () => ({ @@ -55,6 +56,10 @@ vi.mock('../../utils/dotnet/dotnet', () => ({ getDotNetCommand: mocks.getDotNetCommand, })); +vi.mock('../../utils/sdkResolution', () => ({ + resolveSdkFromProject: mocks.resolveSdkFromProject, +})); + vi.mock('fs-extra', () => ({ pathExists: mocks.pathExists, readFile: mocks.readFile, @@ -82,7 +87,6 @@ vi.mock('../../../extensionVariables', () => ({ describe('LogicAppsLanguageServer', () => { const dependenciesPath = 'D:\\dependencies'; const projectPath = 'D:\\workspace\\logic-app'; - const sdkFolderPath = path.join(dependenciesPath, 'LanguageServerLogicApps'); const lspServerPath = path.join(dependenciesPath, 'LSPServer', 'SdkLspServer.dll'); beforeEach(() => { @@ -96,6 +100,7 @@ describe('LogicAppsLanguageServer', () => { mocks.readdir.mockResolvedValue([]); mocks.tryGetLogicAppProjectRoot.mockResolvedValue(projectPath); mocks.getDotNetCommand.mockReturnValue('D:\\dependencies\\DotNetSDK\\dotnet.exe'); + mocks.resolveSdkFromProject.mockResolvedValue(undefined); mocks.getAzureConnectorDetailsForLocalProject.mockResolvedValue({ accessToken: 'Bearer token', resourceGroupName: 'resource-group', @@ -114,13 +119,12 @@ describe('LogicAppsLanguageServer', () => { expect(mocks.showWarningMessage).not.toHaveBeenCalled(); }); - it('does not scan a missing SDK directory', async () => { + it('does not start when SDK is not resolved from project or fallback', async () => { mocks.pathExists.mockImplementation(async (filePath: string) => filePath === lspServerPath); + mocks.resolveSdkFromProject.mockResolvedValue(undefined); await new LogicAppsLanguageServer({} as any).start(); - expect(mocks.pathExists).toHaveBeenCalledWith(sdkFolderPath); - expect(mocks.readdir).not.toHaveBeenCalled(); expect(mocks.getAzureConnectorDetailsForLocalProject).not.toHaveBeenCalled(); expect(mocks.languageClient).not.toHaveBeenCalled(); expect(mocks.showWarningMessage).toHaveBeenCalledWith( @@ -129,12 +133,11 @@ describe('LogicAppsLanguageServer', () => { }); it('does not join an undefined SDK package path when no SDK package is installed', async () => { - mocks.pathExists.mockImplementation(async (filePath: string) => filePath === lspServerPath || filePath === sdkFolderPath); - mocks.readdir.mockResolvedValue(['readme.txt']); + mocks.pathExists.mockImplementation(async (filePath: string) => filePath === lspServerPath); + mocks.resolveSdkFromProject.mockResolvedValue(undefined); await new LogicAppsLanguageServer({} as any).start(); - expect(mocks.readdir).toHaveBeenCalledWith(sdkFolderPath); expect(mocks.getAzureConnectorDetailsForLocalProject).not.toHaveBeenCalled(); expect(mocks.languageClient).not.toHaveBeenCalled(); expect(mocks.showWarningMessage).toHaveBeenCalledWith( @@ -142,14 +145,16 @@ describe('LogicAppsLanguageServer', () => { ); }); - it('starts the language client when project and language server dependencies are available', async () => { + it('starts the language client when SDK is resolved from the NuGet cache', async () => { + const resolvedNupkgPath = 'C:\\Users\\user\\.nuget\\packages\\microsoft.azure.workflows.sdk\\1.0.0-preview.2\\microsoft.azure.workflows.sdk.1.0.0-preview.2.nupkg'; const languageClient = { start: vi.fn().mockResolvedValue(undefined) }; mocks.languageClient.mockReturnValue(languageClient); - mocks.pathExists.mockImplementation(async (filePath: string) => filePath === lspServerPath || filePath === sdkFolderPath); - mocks.readdir.mockResolvedValue(['Microsoft.Azure.Workflows.Sdk.1.0.0-preview.1.nupkg']); + mocks.pathExists.mockImplementation(async (filePath: string) => filePath === lspServerPath); + mocks.resolveSdkFromProject.mockResolvedValue({ sdkNupkgPath: resolvedNupkgPath, version: '1.0.0-preview.2' }); await new LogicAppsLanguageServer({} as any).start(); + expect(mocks.resolveSdkFromProject).toHaveBeenCalledWith(projectPath); expect(mocks.getAzureConnectorDetailsForLocalProject).toHaveBeenCalledWith(expect.any(Object), projectPath); expect(mocks.languageClient).toHaveBeenCalledWith( 'logicAppsLanguageServer', @@ -157,11 +162,11 @@ describe('LogicAppsLanguageServer', () => { { run: { command: 'D:\\dependencies\\DotNetSDK\\dotnet.exe', - args: [lspServerPath, '--sdk', path.join(sdkFolderPath, 'Microsoft.Azure.Workflows.Sdk.1.0.0-preview.1.nupkg')], + args: [lspServerPath, '--sdk', resolvedNupkgPath], }, debug: { command: 'D:\\dependencies\\DotNetSDK\\dotnet.exe', - args: [lspServerPath, '--sdk', path.join(sdkFolderPath, 'Microsoft.Azure.Workflows.Sdk.1.0.0-preview.1.nupkg')], + args: [lspServerPath, '--sdk', resolvedNupkgPath], }, }, expect.objectContaining({ @@ -176,4 +181,5 @@ describe('LogicAppsLanguageServer', () => { ); expect(languageClient.start).toHaveBeenCalledOnce(); }); + }); diff --git a/apps/vs-code-designer/src/app/languageServer/languageServer.ts b/apps/vs-code-designer/src/app/languageServer/languageServer.ts index 4adaf4f7c87..24824733726 100644 --- a/apps/vs-code-designer/src/app/languageServer/languageServer.ts +++ b/apps/vs-code-designer/src/app/languageServer/languageServer.ts @@ -3,7 +3,6 @@ import { callWithTelemetryAndErrorHandling } from '@microsoft/vscode-azext-utils import { autoRuntimeDependenciesPathSettingKey, connectionsFileName, - lspDirectory, onStartLanguageServer, workflowAppApiVersion, } from '../../constants'; @@ -21,9 +20,11 @@ import { getAzureConnectorDetailsForLocalProject } from '../utils/codeless/commo import * as vscode from 'vscode'; import { filterCompletionResult } from './completionFilter'; import { getDotNetCommand } from '../utils/dotnet/dotnet'; +import { resolveSdkFromProject } from '../utils/sdkResolution'; +import { localize } from '../../localize'; export default class LogicAppsLanguageServer { - protected lspServerPath: string | undefined; + protected lspServerDllPath: string | undefined; protected sdkNupkgPath: string | undefined; protected apiVersion = workflowAppApiVersion; private projectPath: string | undefined; @@ -45,12 +46,12 @@ export default class LogicAppsLanguageServer { return; } - const { lspServerPath, sdkNupkgPath } = await this.getSDKPaths(); + const { lspServerDllPath, sdkNupkgPath } = await this.getSDKPaths(); - this.lspServerPath = lspServerPath; + this.lspServerDllPath = lspServerDllPath; this.sdkNupkgPath = sdkNupkgPath; - if (!this.lspServerPath) { + if (!this.lspServerDllPath) { window.showWarningMessage('Install or repair Logic Apps language server dependencies before starting C# workflow authoring.'); return; } @@ -63,7 +64,7 @@ export default class LogicAppsLanguageServer { const metaData = await this.getMetadata(); // Build server arguments (removed --connections) - const serverArgs = [this.lspServerPath, '--sdk', this.sdkNupkgPath]; + const serverArgs = [this.lspServerDllPath, '--sdk', this.sdkNupkgPath]; // Load connections from file const connections = await this.loadConnectionsFromFile(metaData.connectionFilePath); @@ -174,39 +175,23 @@ export default class LogicAppsLanguageServer { private async getSDKPaths() { const dependenciesPath = getGlobalSetting(autoRuntimeDependenciesPathSettingKey); if (!dependenciesPath) { - return { lspServerPath: undefined, sdkNupkgPath: undefined }; + ext.outputChannel.appendLog(localize('dependenciesPathNotSet', '[LSP] Could not resolve SDK paths. The dependencies path is not set.')); + return { lspServerDllPath: undefined, sdkNupkgPath: undefined }; } - // Support for development mode - override with environment variable - const devLspServerPath = process.env.LSP_SERVER_DEV_PATH; - let lspServerPath: string | undefined; - - if (devLspServerPath && (await fse.pathExists(devLspServerPath))) { - lspServerPath = devLspServerPath; - console.log(`[LSP] Using development server: ${lspServerPath}`); - } else { - lspServerPath = path.join(dependenciesPath, 'LSPServer', 'SdkLspServer.dll'); - if (!(await fse.pathExists(lspServerPath))) { - lspServerPath = undefined; - } + const lspServerDllPath = path.join(dependenciesPath, 'LSPServer', 'SdkLspServer.dll'); + if (!(await fse.pathExists(lspServerDllPath))) { + ext.outputChannel.appendLog(localize('lspServerDllNotFound', '[LSP] Could not find LSP server DLL at "{0}".', lspServerDllPath)); + return { lspServerDllPath: undefined, sdkNupkgPath: undefined }; } - const sdkFolderPath = path.join(dependenciesPath, lspDirectory); - if (!(await fse.pathExists(sdkFolderPath))) { - return { lspServerPath, sdkNupkgPath: undefined }; + const resolvedSdk = await resolveSdkFromProject(this.projectPath!); + const sdkNupkgPath = resolvedSdk ? resolvedSdk.sdkNupkgPath : undefined; + if (!sdkNupkgPath) { + ext.outputChannel.appendLog(localize('sdkNupkgNotFound', '[LSP] Could not resolve SDK nupkg for project at "{0}".', this.projectPath)); } - const files = await fse.readdir(sdkFolderPath); - const sdkNupkgFile = files.find((file) => { - return file.startsWith('Microsoft.Azure.Workflows.Sdk.') && file.endsWith('.nupkg'); - }); - if (!sdkNupkgFile) { - return { lspServerPath, sdkNupkgPath: undefined }; - } - - const sdkNupkgPath = path.join(sdkFolderPath, sdkNupkgFile); - - return { lspServerPath, sdkNupkgPath }; + return { lspServerDllPath, sdkNupkgPath }; } private async getMetadata() { diff --git a/apps/vs-code-designer/src/app/utils/__test__/codeful.test.ts b/apps/vs-code-designer/src/app/utils/__test__/codeful.test.ts index bfa48b0b930..07da295f361 100644 --- a/apps/vs-code-designer/src/app/utils/__test__/codeful.test.ts +++ b/apps/vs-code-designer/src/app/utils/__test__/codeful.test.ts @@ -1,18 +1,13 @@ import path from 'path'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { lspDirectory } from '../../../constants'; -import { codefulProjectsExist, invalidateCodefulSdkCacheIfNeeded, parseCsprojCopyToCodefulInfo } from '../codeful'; +import { codefulProjectsExist, parseCsprojCopyToCodefulInfo } from '../codeful'; const mocks = vi.hoisted(() => ({ - ensureDir: vi.fn(), - getGlobalSetting: vi.fn(), pathExists: vi.fn(), - readdir: vi.fn(), readFile: vi.fn(), - remove: vi.fn(), statSync: vi.fn(), + readdir: vi.fn(), workspaceFolders: undefined as { uri: { fsPath: string } }[] | undefined, - writeFile: vi.fn(), })); vi.mock('vscode', () => ({ @@ -24,150 +19,12 @@ vi.mock('vscode', () => ({ })); vi.mock('fs-extra', () => ({ - ensureDir: mocks.ensureDir, pathExists: mocks.pathExists, readdir: mocks.readdir, readFile: mocks.readFile, - remove: mocks.remove, statSync: mocks.statSync, - writeFile: mocks.writeFile, -})); - -vi.mock('../vsCodeConfig/settings', () => ({ - getGlobalSetting: mocks.getGlobalSetting, })); -vi.mock('../../../extensionVariables', () => ({ - ext: { - outputChannel: { - appendLog: vi.fn(), - }, - }, -})); - -describe('invalidateCodefulSdkCacheIfNeeded', () => { - const projectPath = 'D:\\workspace\\CodefulLogicApp'; - const runtimeDependenciesPath = 'D:\\runtime-dependencies'; - const lspDirectoryPath = path.join(runtimeDependenciesPath, lspDirectory); - const csprojPath = path.join(projectPath, 'CodefulLogicApp.csproj'); - const nugetConfigPath = path.join(projectPath, 'nuget.config'); - const installedSdkHashMarkerPath = path.join(runtimeDependenciesPath, '.lspsdk-hash'); - const projectSdkHashMarkerPath = path.join(projectPath, '.nuget', '.logicapps-lspsdk-hash'); - const projectSdkPackagePath = path.join(projectPath, '.nuget', 'packages', 'microsoft.azure.workflows.sdk', '1.0.0-preview.1'); - const projectAssetsPath = path.join(projectPath, 'obj', 'project.assets.json'); - const projectNugetCachePath = path.join(projectPath, 'obj', 'project.nuget.cache'); - const currentSdkHash = 'current-sdk-hash'; - const csprojContent = ` - - - net8 - - - - -`; - const codefulNugetConfig = ` - - - - - - - - - -`; - - beforeEach(() => { - vi.clearAllMocks(); - mocks.getGlobalSetting.mockReturnValue(runtimeDependenciesPath); - mocks.ensureDir.mockResolvedValue(undefined); - mocks.remove.mockResolvedValue(undefined); - mocks.writeFile.mockResolvedValue(undefined); - mocks.readdir.mockResolvedValue(['CodefulLogicApp.csproj']); - mocks.statSync.mockReturnValue({ isDirectory: () => true }); - setExistingPaths([ - csprojPath, - nugetConfigPath, - installedSdkHashMarkerPath, - projectSdkPackagePath, - projectAssetsPath, - projectNugetCachePath, - ]); - mocks.readFile.mockImplementation(async (filePath: string) => { - if (filePath === csprojPath) { - return csprojContent; - } - if (filePath === nugetConfigPath) { - return codefulNugetConfig; - } - if (filePath === installedSdkHashMarkerPath) { - return currentSdkHash; - } - return ''; - }); - }); - - function setExistingPaths(paths: string[]): void { - const existingPaths = new Set(paths); - mocks.pathExists.mockImplementation(async (filePath: string) => existingPaths.has(filePath)); - } - - it('removes only the stale project-local SDK package when the installed VSIX SDK hash changes', async () => { - const invalidated = await invalidateCodefulSdkCacheIfNeeded(projectPath); - - expect(invalidated).toBe(true); - expect(mocks.remove).toHaveBeenCalledWith(projectSdkPackagePath); - expect(mocks.remove).toHaveBeenCalledWith(projectAssetsPath); - expect(mocks.remove).toHaveBeenCalledWith(projectNugetCachePath); - expect(mocks.ensureDir).toHaveBeenCalledWith(path.join(projectPath, '.nuget')); - expect(mocks.writeFile).toHaveBeenCalledWith(projectSdkHashMarkerPath, currentSdkHash); - }); - - it('keeps the project cache when its marker already matches the installed SDK hash', async () => { - setExistingPaths([csprojPath, nugetConfigPath, installedSdkHashMarkerPath, projectSdkHashMarkerPath, projectSdkPackagePath]); - mocks.readFile.mockImplementation(async (filePath: string) => { - if (filePath === csprojPath) { - return csprojContent; - } - if (filePath === nugetConfigPath) { - return codefulNugetConfig; - } - if (filePath === installedSdkHashMarkerPath || filePath === projectSdkHashMarkerPath) { - return currentSdkHash; - } - return ''; - }); - - const invalidated = await invalidateCodefulSdkCacheIfNeeded(projectPath); - - expect(invalidated).toBe(false); - expect(mocks.remove).not.toHaveBeenCalled(); - expect(mocks.writeFile).not.toHaveBeenCalled(); - }); - - it('does not touch caches for projects that do not use the extension local SDK source and project-local packages folder', async () => { - mocks.readFile.mockImplementation(async (filePath: string) => { - if (filePath === csprojPath) { - return csprojContent; - } - if (filePath === nugetConfigPath) { - return codefulNugetConfig.replace(lspDirectoryPath, 'https://api.nuget.org/v3/index.json'); - } - if (filePath === installedSdkHashMarkerPath) { - return currentSdkHash; - } - return ''; - }); - - const invalidated = await invalidateCodefulSdkCacheIfNeeded(projectPath); - - expect(invalidated).toBe(false); - expect(mocks.remove).not.toHaveBeenCalled(); - expect(mocks.writeFile).not.toHaveBeenCalled(); - }); -}); - describe('parseCsprojCopyToCodefulInfo', () => { it('detects modern codeful targets that run on Build and Publish', () => { const info = parseCsprojCopyToCodefulInfo(` diff --git a/apps/vs-code-designer/src/app/utils/__test__/languageServerProtocol.test.ts b/apps/vs-code-designer/src/app/utils/__test__/languageServerProtocol.test.ts index 1292d5db6af..35353ae40fe 100644 --- a/apps/vs-code-designer/src/app/utils/__test__/languageServerProtocol.test.ts +++ b/apps/vs-code-designer/src/app/utils/__test__/languageServerProtocol.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { autoRuntimeDependenciesPathSettingKey, defaultDependencyPathValue, lspDirectory } from '../../../constants'; +import { autoRuntimeDependenciesPathSettingKey, defaultDependencyPathValue } from '../../../constants'; import { ext } from '../../../extensionVariables'; -import { installLSPSDK } from '../languageServerProtocol'; +import { installLSPServer } from '../languageServerProtocol'; import path from 'path'; import { createHash } from 'crypto'; @@ -11,7 +11,6 @@ const mocks = vi.hoisted(() => { return { admZip, - copyFile: vi.fn(), ensureDir: vi.fn(), extractAllTo, getGlobalSetting: vi.fn(), @@ -19,7 +18,6 @@ const mocks = vi.hoisted(() => { readdir: vi.fn(), readFile: vi.fn(), remove: vi.fn(), - stat: vi.fn(), updateGlobalSetting: vi.fn(), writeFile: vi.fn(), }; @@ -30,13 +28,11 @@ vi.mock('adm-zip', () => ({ })); vi.mock('fs-extra', () => ({ - copyFile: mocks.copyFile, ensureDir: mocks.ensureDir, pathExists: mocks.pathExists, readdir: mocks.readdir, readFile: mocks.readFile, remove: mocks.remove, - stat: mocks.stat, writeFile: mocks.writeFile, })); @@ -51,24 +47,15 @@ vi.mock('../../../extensionVariables', () => ({ }, })); -describe('installLSPSDK', () => { +describe('installLSPServer', () => { const targetDirectory = 'D:\\runtime-dependencies'; const lspServerPath = path.join(targetDirectory, 'LSPServer'); const lspServerDllPath = path.join(lspServerPath, 'SdkLspServer.dll'); const lspHashMarker = path.join(targetDirectory, '.lspserver-hash'); - const sdkDirectoryPath = path.join(targetDirectory, lspDirectory); - const sdkHashMarker = path.join(targetDirectory, '.lspsdk-hash'); - const sdkPackageName = 'Microsoft.Azure.Workflows.Sdk.1.0.0-preview.1.nupkg'; - const sdkDestinationFile = path.join(sdkDirectoryPath, sdkPackageName); const legacyLspVersionMarker = path.join(targetDirectory, '.lspserver-version'); - const legacyLspPathMarker = path.join(targetDirectory, '.lspserver-path'); - const legacySdkVersionMarker = path.join(targetDirectory, '.lspsdk-version'); const staleVersionedLspFolder = path.join(targetDirectory, 'LSPServer-1778130324219'); const serverZipContent = Buffer.from('server zip content'); - const sdkPackageContent = Buffer.from('sdk package content'); - const staleSdkPackageContent = Buffer.from('stale sdk package content'); const serverZipHash = createHash('sha256').update(serverZipContent).digest('hex'); - const sdkPackageHash = createHash('sha256').update(sdkPackageContent).digest('hex'); const oldHash = 'old-hash'; let lspServerExtracted = false; @@ -76,7 +63,6 @@ describe('installLSPSDK', () => { vi.clearAllMocks(); mocks.extractAllTo.mockReset(); mocks.getGlobalSetting.mockReturnValue(targetDirectory); - mocks.copyFile.mockResolvedValue(undefined); mocks.ensureDir.mockResolvedValue(undefined); mocks.extractAllTo.mockImplementation(() => { lspServerExtracted = true; @@ -98,134 +84,92 @@ describe('installLSPSDK', () => { }); } - function configureReadFileMocks(options?: { - lspHashMarker?: string; - sdkHashMarker?: string; - installedSdkContent?: Buffer; - }): void { + function configureReadFileMocks(options?: { lspHashMarker?: string }): void { mocks.readFile.mockImplementation(async (filePath: string, encoding?: BufferEncoding) => { if (encoding === 'utf-8') { if (filePath === lspHashMarker) { return options?.lspHashMarker ?? oldHash; } - if (filePath === sdkHashMarker) { - return options?.sdkHashMarker ?? oldHash; - } return ''; } if (filePath.includes('LSPServer.zip')) { return serverZipContent; } - if (filePath === sdkDestinationFile) { - return options?.installedSdkContent ?? staleSdkPackageContent; - } - if (filePath.includes(sdkPackageName)) { - return sdkPackageContent; - } return Buffer.from(''); }); } - it('extracts the LSP server and copies the SDK when target directories are missing', async () => { + it('extracts the LSP server when target directory is missing', async () => { setExistingPaths([]); - await installLSPSDK(); + await installLSPServer(); expect(mocks.ensureDir).toHaveBeenCalledWith(targetDirectory); expect(mocks.admZip).toHaveBeenCalledWith(expect.stringContaining('LSPServer.zip')); expect(mocks.extractAllTo).toHaveBeenCalledWith(targetDirectory, true, true); - expect(mocks.ensureDir).toHaveBeenCalledWith(sdkDirectoryPath); - expect(mocks.copyFile).toHaveBeenCalledWith(expect.stringContaining(sdkPackageName), sdkDestinationFile); expect(mocks.writeFile).toHaveBeenCalledWith(lspHashMarker, serverZipHash); - expect(mocks.writeFile).toHaveBeenCalledWith(sdkHashMarker, sdkPackageHash); }); - it('defaults the dependencies path before extracting LSP assets when the setting is unset', async () => { + it('defaults the dependencies path before extracting when the setting is unset', async () => { mocks.getGlobalSetting.mockReturnValue(undefined); setExistingPaths([]); - await installLSPSDK(); + await installLSPServer(); expect(mocks.updateGlobalSetting).toHaveBeenCalledWith(autoRuntimeDependenciesPathSettingKey, defaultDependencyPathValue); expect(mocks.ensureDir).toHaveBeenCalledWith(defaultDependencyPathValue); expect(mocks.extractAllTo).toHaveBeenCalledWith(defaultDependencyPathValue, true, true); }); - it('updates both assets when target files exist but hash markers are missing', async () => { - setExistingPaths([lspServerPath, lspServerDllPath, sdkDestinationFile]); + it('extracts when target file exists but hash marker is missing', async () => { + setExistingPaths([lspServerPath, lspServerDllPath]); - await installLSPSDK(); + await installLSPServer(); expect(mocks.remove).toHaveBeenCalledWith(lspServerPath); expect(mocks.extractAllTo).toHaveBeenCalledOnce(); - expect(mocks.copyFile).toHaveBeenCalledOnce(); }); - it('updates both assets when source hashes differ from their stored markers', async () => { - setExistingPaths([lspServerPath, lspServerDllPath, lspHashMarker, sdkDestinationFile, sdkHashMarker]); + it('extracts when source hash differs from stored marker', async () => { + setExistingPaths([lspServerPath, lspServerDllPath, lspHashMarker]); - await installLSPSDK(); + await installLSPServer(); expect(mocks.extractAllTo).toHaveBeenCalledOnce(); - expect(mocks.copyFile).toHaveBeenCalledOnce(); expect(mocks.writeFile).toHaveBeenCalledWith(lspHashMarker, serverZipHash); - expect(mocks.writeFile).toHaveBeenCalledWith(sdkHashMarker, sdkPackageHash); }); - it('does not update assets when hash markers and installed SDK package are current', async () => { - setExistingPaths([lspServerDllPath, lspHashMarker, sdkDestinationFile, sdkHashMarker]); - configureReadFileMocks({ - lspHashMarker: serverZipHash, - sdkHashMarker: sdkPackageHash, - installedSdkContent: sdkPackageContent, - }); + it('does not extract when hash marker is current', async () => { + setExistingPaths([lspServerDllPath, lspHashMarker]); + configureReadFileMocks({ lspHashMarker: serverZipHash }); - await installLSPSDK(); + await installLSPServer(); expect(mocks.extractAllTo).not.toHaveBeenCalled(); - expect(mocks.copyFile).not.toHaveBeenCalled(); expect(mocks.writeFile).not.toHaveBeenCalled(); expect(mocks.ensureDir).toHaveBeenCalledTimes(1); expect(mocks.ensureDir).toHaveBeenCalledWith(targetDirectory); }); - it('copies the SDK when the marker is current but the installed same-version package is stale', async () => { - setExistingPaths([lspServerDllPath, lspHashMarker, sdkDestinationFile, sdkHashMarker]); - configureReadFileMocks({ - lspHashMarker: serverZipHash, - sdkHashMarker: sdkPackageHash, - installedSdkContent: staleSdkPackageContent, - }); - - await installLSPSDK(); - - expect(mocks.extractAllTo).not.toHaveBeenCalled(); - expect(mocks.copyFile).toHaveBeenCalledWith(expect.stringContaining(sdkPackageName), sdkDestinationFile); - expect(mocks.writeFile).toHaveBeenCalledWith(sdkHashMarker, sdkPackageHash); - }); - - it('treats legacy mtime markers as stale and refreshes assets', async () => { - setExistingPaths([lspServerPath, lspServerDllPath, legacyLspVersionMarker, sdkDestinationFile, legacySdkVersionMarker]); + it('treats legacy mtime markers as stale and refreshes', async () => { + setExistingPaths([lspServerPath, lspServerDllPath, legacyLspVersionMarker]); configureReadFileMocks({ lspHashMarker: '2026-05-06T09:17:03.798Z', - sdkHashMarker: '2026-05-06T09:17:03.860Z', }); - await installLSPSDK(); + await installLSPServer(); expect(mocks.extractAllTo).toHaveBeenCalledOnce(); - expect(mocks.copyFile).toHaveBeenCalledOnce(); expect(mocks.writeFile).toHaveBeenCalledWith(lspHashMarker, serverZipHash); - expect(mocks.writeFile).toHaveBeenCalledWith(sdkHashMarker, sdkPackageHash); }); - it('cleans stale versioned LSP folders after refreshing the stable LSP folder', async () => { + it('cleans stale versioned LSP folders after refreshing', async () => { setExistingPaths([lspServerPath]); - mocks.readdir.mockResolvedValue(['LSPServer', 'LSPServer-1778130324219', 'LanguageServerLogicApps']); + mocks.readdir.mockResolvedValue(['LSPServer', 'LSPServer-1778130324219', 'OtherFolder']); - await installLSPSDK(); + await installLSPServer(); expect(mocks.remove).toHaveBeenCalledWith(staleVersionedLspFolder); }); @@ -235,8 +179,7 @@ describe('installLSPSDK', () => { throw new Error('zip failed'); }); - await expect(installLSPSDK()).rejects.toThrow('Error extracting LSP server: Error: zip failed'); - expect(mocks.copyFile).not.toHaveBeenCalled(); + await expect(installLSPServer()).rejects.toThrow('Error extracting LSP server: Error: zip failed'); }); it('adds actionable guidance for locked LSP files during stable folder removal', async () => { @@ -250,9 +193,8 @@ describe('installLSPSDK', () => { } }); - await expect(installLSPSDK()).rejects.toThrow('stop the dotnet process running SdkLspServer.dll'); + await expect(installLSPServer()).rejects.toThrow('stop the dotnet process running SdkLspServer.dll'); expect(mocks.extractAllTo).not.toHaveBeenCalled(); - expect(mocks.copyFile).not.toHaveBeenCalled(); }); it('adds actionable guidance for locked LSP files during extraction', async () => { @@ -263,8 +205,7 @@ describe('installLSPSDK', () => { throw lockedError; }); - await expect(installLSPSDK()).rejects.toThrow('stop the dotnet process running SdkLspServer.dll'); - expect(mocks.copyFile).not.toHaveBeenCalled(); + await expect(installLSPServer()).rejects.toThrow('stop the dotnet process running SdkLspServer.dll'); }); it('stops a running language client before replacing LSP assets', async () => { @@ -272,32 +213,19 @@ describe('installLSPSDK', () => { ext.languageClient = { stop } as any; setExistingPaths([]); - await installLSPSDK(); + await installLSPServer(); expect(stop).toHaveBeenCalledOnce(); expect(ext.languageClient).toBeUndefined(); expect(stop.mock.invocationCallOrder[0]).toBeLessThan(mocks.extractAllTo.mock.invocationCallOrder[0]); - expect(stop.mock.invocationCallOrder[0]).toBeLessThan(mocks.copyFile.mock.invocationCallOrder[0]); }); - it('does not extract or copy assets when stopping the language client fails', async () => { + it('does not extract when stopping the language client fails', async () => { const stop = vi.fn().mockRejectedValue(new Error('stop failed')); ext.languageClient = { stop } as any; - await expect(installLSPSDK()).rejects.toThrow('Error stopping LSP server before update: Error: stop failed'); + await expect(installLSPServer()).rejects.toThrow('Error stopping LSP server before update: Error: stop failed'); expect(mocks.extractAllTo).not.toHaveBeenCalled(); - expect(mocks.copyFile).not.toHaveBeenCalled(); expect(ext.languageClient).toBeDefined(); }); - - it('wraps SDK copy errors with SDK context', async () => { - setExistingPaths([lspServerDllPath, lspHashMarker]); - configureReadFileMocks({ - lspHashMarker: serverZipHash, - }); - mocks.copyFile.mockRejectedValue(new Error('copy failed')); - - await expect(installLSPSDK()).rejects.toThrow('Error copying sdk: Error: copy failed'); - expect(mocks.extractAllTo).not.toHaveBeenCalled(); - }); }); diff --git a/apps/vs-code-designer/src/app/utils/__test__/sdkResolution.test.ts b/apps/vs-code-designer/src/app/utils/__test__/sdkResolution.test.ts new file mode 100644 index 00000000000..f1180380f8c --- /dev/null +++ b/apps/vs-code-designer/src/app/utils/__test__/sdkResolution.test.ts @@ -0,0 +1,357 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { resolveSdkFromProject } from '../sdkResolution'; +import path from 'path'; + +const mocks = vi.hoisted(() => ({ + appendLog: vi.fn(), + executeCommand: vi.fn(), + getDotNetCommand: vi.fn(), + pathExists: vi.fn(), + readdir: vi.fn(), + readFile: vi.fn(), +})); + +vi.mock('fs-extra', () => ({ + pathExists: mocks.pathExists, + readdir: mocks.readdir, + readFile: mocks.readFile, +})); + +vi.mock('../dotnet/dotnet', () => ({ + getDotNetCommand: mocks.getDotNetCommand, +})); + +vi.mock('../funcCoreTools/cpUtils', () => ({ + executeCommand: mocks.executeCommand, +})); + +vi.mock('../../../extensionVariables', () => ({ + ext: { + outputChannel: { appendLog: mocks.appendLog }, + }, +})); + +const projectPath = 'D:\\workspace\\MyLogicApp'; +const csprojFileName = 'MyLogicApp.csproj'; +const csprojFilePath = path.join(projectPath, csprojFileName); +const sdkVersion = '1.0.0-preview.2'; +const nugetCacheRoot = 'C:\\Users\\user\\.nuget\\packages'; +const expectedNupkgPath = path.join( + nugetCacheRoot, + 'microsoft.azure.workflows.sdk', + sdkVersion, + `microsoft.azure.workflows.sdk.${sdkVersion}.nupkg` +); + +function makeCsproj(packageRefs: string): string { + return ` + + net8.0 + + + ${packageRefs} + +`; +} + +function setExistingPaths(paths: string[]): void { + const existingPaths = new Set(paths); + mocks.pathExists.mockImplementation(async (p: string) => existingPaths.has(p)); +} + +describe('resolveSdkFromProject', () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.getDotNetCommand.mockReturnValue('dotnet'); + mocks.readdir.mockResolvedValue([csprojFileName]); + mocks.readFile.mockResolvedValue( + makeCsproj(``) + ); + mocks.executeCommand.mockResolvedValue(`global-packages: ${nugetCacheRoot}\n`); + setExistingPaths([expectedNupkgPath, nugetCacheRoot]); + }); + + // --- Happy path --- + + it('resolves SDK from the global NuGet cache', async () => { + const result = await resolveSdkFromProject(projectPath); + + expect(result).toEqual({ sdkNupkgPath: expectedNupkgPath, version: sdkVersion }); + expect(mocks.executeCommand).toHaveBeenCalledWith( + expect.anything(), + undefined, + 'dotnet', + 'nuget', + 'locals', + 'global-packages', + '--list' + ); + }); + + it('uses the configured dotnet binary path', async () => { + mocks.getDotNetCommand.mockReturnValue('D:\\dependencies\\DotNetSDK\\dotnet.exe'); + + await resolveSdkFromProject(projectPath); + + expect(mocks.executeCommand).toHaveBeenCalledWith( + expect.anything(), + undefined, + 'D:\\dependencies\\DotNetSDK\\dotnet.exe', + 'nuget', + 'locals', + 'global-packages', + '--list' + ); + }); + + // --- .csproj discovery --- + + it('returns undefined when project folder has no .csproj file', async () => { + mocks.readdir.mockResolvedValue(['Program.cs', 'host.json']); + + const result = await resolveSdkFromProject(projectPath); + + expect(result).toBeUndefined(); + expect(mocks.appendLog).toHaveBeenCalledWith(expect.stringContaining('No .csproj file found')); + }); + + it('returns undefined when readdir throws (missing folder)', async () => { + mocks.readdir.mockRejectedValue(new Error('ENOENT')); + + const result = await resolveSdkFromProject(projectPath); + + expect(result).toBeUndefined(); + }); + + it('picks the first .csproj when multiple exist', async () => { + const otherVersion = '1.0.0-preview.1'; + const otherNupkgPath = path.join( + nugetCacheRoot, + 'microsoft.azure.workflows.sdk', + otherVersion, + `microsoft.azure.workflows.sdk.${otherVersion}.nupkg` + ); + mocks.readdir.mockResolvedValue(['Other.csproj', 'MyLogicApp.csproj']); + mocks.readFile.mockImplementation(async (filePath: string) => { + if (filePath === path.join(projectPath, 'Other.csproj')) { + return makeCsproj(``); + } + return makeCsproj(``); + }); + setExistingPaths([nugetCacheRoot, otherNupkgPath]); + + const result = await resolveSdkFromProject(projectPath); + + // Should use the first .csproj found (Other.csproj) + expect(result?.version).toBe(otherVersion); + }); + + // --- .csproj parsing --- + + it('returns undefined when .csproj has no SDK PackageReference', async () => { + mocks.readFile.mockResolvedValue( + makeCsproj(``) + ); + + const result = await resolveSdkFromProject(projectPath); + + expect(result).toBeUndefined(); + expect(mocks.appendLog).toHaveBeenCalledWith(expect.stringContaining('No Microsoft.Azure.Workflows.Sdk PackageReference')); + }); + + it('matches SDK PackageReference case-insensitively', async () => { + mocks.readFile.mockResolvedValue( + makeCsproj(``) + ); + + const result = await resolveSdkFromProject(projectPath); + + expect(result).toEqual({ sdkNupkgPath: expectedNupkgPath, version: sdkVersion }); + }); + + it('returns undefined when version is an MSBuild variable', async () => { + mocks.readFile.mockResolvedValue( + makeCsproj(``) + ); + + const result = await resolveSdkFromProject(projectPath); + + expect(result).toBeUndefined(); + expect(mocks.appendLog).toHaveBeenCalledWith(expect.stringContaining('MSBuild variable')); + }); + + it('returns undefined when version is an MSBuild item reference', async () => { + mocks.readFile.mockResolvedValue( + makeCsproj(``) + ); + + const result = await resolveSdkFromProject(projectPath); + + expect(result).toBeUndefined(); + }); + + it('returns undefined when Version attribute is empty', async () => { + mocks.readFile.mockResolvedValue( + makeCsproj(``) + ); + + const result = await resolveSdkFromProject(projectPath); + + expect(result).toBeUndefined(); + }); + + it('returns undefined for malformed XML', async () => { + mocks.readFile.mockResolvedValue(' { + mocks.readFile.mockResolvedValue(` + + + + + + +`); + + const result = await resolveSdkFromProject(projectPath); + + expect(result?.version).toBe(sdkVersion); + }); + + it('trims whitespace from version strings', async () => { + mocks.readFile.mockResolvedValue( + makeCsproj(``) + ); + + const result = await resolveSdkFromProject(projectPath); + + expect(result?.version).toBe(sdkVersion); + }); + + it('returns undefined when readFile throws', async () => { + mocks.readFile.mockRejectedValue(new Error('EACCES')); + + const result = await resolveSdkFromProject(projectPath); + + expect(result).toBeUndefined(); + }); + + // --- NuGet cache resolution --- + + it('parses dotnet nuget locals output with trailing whitespace', async () => { + mocks.executeCommand.mockResolvedValue(`global-packages: ${nugetCacheRoot} \r\n`); + + const result = await resolveSdkFromProject(projectPath); + + expect(result).toEqual({ sdkNupkgPath: expectedNupkgPath, version: sdkVersion }); + }); + + it('returns undefined when dotnet nuget locals returns empty output', async () => { + mocks.executeCommand.mockResolvedValue(''); + + const result = await resolveSdkFromProject(projectPath); + + expect(result).toBeUndefined(); + }); + + it('returns undefined when dotnet nuget locals returns unexpected format', async () => { + mocks.executeCommand.mockResolvedValue('some unexpected output\n'); + + const result = await resolveSdkFromProject(projectPath); + + expect(result).toBeUndefined(); + expect(mocks.appendLog).toHaveBeenCalledWith(expect.stringContaining('Unexpected dotnet nuget locals output')); + }); + + it('returns undefined when cache path does not exist on disk', async () => { + setExistingPaths([]); // nothing exists + + const result = await resolveSdkFromProject(projectPath); + + expect(result).toBeUndefined(); + }); + + // --- Fallback: project-local cache --- + + it('falls back to project-local .nuget/packages when global cache has no nupkg', async () => { + const localNupkgPath = path.join( + projectPath, + '.nuget', + 'packages', + 'microsoft.azure.workflows.sdk', + sdkVersion, + `microsoft.azure.workflows.sdk.${sdkVersion}.nupkg` + ); + setExistingPaths([nugetCacheRoot, localNupkgPath]); + + const result = await resolveSdkFromProject(projectPath); + + expect(result).toEqual({ sdkNupkgPath: localNupkgPath, version: sdkVersion }); + }); + + it('returns undefined when nupkg is in neither global nor project-local cache', async () => { + setExistingPaths([nugetCacheRoot]); // cache exists but nupkg doesn't + + const result = await resolveSdkFromProject(projectPath); + + expect(result).toBeUndefined(); + expect(mocks.appendLog).toHaveBeenCalledWith(expect.stringContaining("Ensure 'dotnet restore' has been run")); + }); + + // --- Fallback: dotnet CLI failure --- + + it('falls back to NUGET_PACKAGES env var when dotnet command throws', async () => { + const envCachePath = 'D:\\custom-nuget-cache'; + const envNupkgPath = path.join( + envCachePath, + 'microsoft.azure.workflows.sdk', + sdkVersion, + `microsoft.azure.workflows.sdk.${sdkVersion}.nupkg` + ); + process.env.NUGET_PACKAGES = envCachePath; + mocks.executeCommand.mockRejectedValue(new Error('dotnet not found')); + setExistingPaths([envCachePath, envNupkgPath]); + + const result = await resolveSdkFromProject(projectPath); + + expect(result).toEqual({ sdkNupkgPath: envNupkgPath, version: sdkVersion }); + delete process.env.NUGET_PACKAGES; + }); + + it('falls back to default home .nuget/packages when dotnet CLI and NUGET_PACKAGES both fail', async () => { + const home = process.env.USERPROFILE || process.env.HOME; + if (!home) { + // Skip this test if no home directory is available + return; + } + const defaultCachePath = path.join(home, '.nuget', 'packages'); + const defaultNupkgPath = path.join( + defaultCachePath, + 'microsoft.azure.workflows.sdk', + sdkVersion, + `microsoft.azure.workflows.sdk.${sdkVersion}.nupkg` + ); + delete process.env.NUGET_PACKAGES; + mocks.executeCommand.mockRejectedValue(new Error('dotnet not found')); + setExistingPaths([defaultCachePath, defaultNupkgPath]); + + const result = await resolveSdkFromProject(projectPath); + + expect(result).toEqual({ sdkNupkgPath: defaultNupkgPath, version: sdkVersion }); + }); + + it('returns undefined when all fallbacks fail', async () => { + delete process.env.NUGET_PACKAGES; + mocks.executeCommand.mockRejectedValue(new Error('dotnet not found')); + setExistingPaths([]); // nothing exists + + const result = await resolveSdkFromProject(projectPath); + + expect(result).toBeUndefined(); + }); +}); diff --git a/apps/vs-code-designer/src/app/utils/codeful.ts b/apps/vs-code-designer/src/app/utils/codeful.ts index fd0746d764d..460f7650b2f 100644 --- a/apps/vs-code-designer/src/app/utils/codeful.ts +++ b/apps/vs-code-designer/src/app/utils/codeful.ts @@ -1,14 +1,7 @@ import path from 'path'; import * as fse from 'fs-extra'; import * as vscode from 'vscode'; -import { autoRuntimeDependenciesPathSettingKey, localSettingsFileName, lspDirectory } from '../../constants'; -import { ext } from '../../extensionVariables'; -import { getGlobalSetting } from './vsCodeConfig/settings'; - -const codefulSdkPackageId = 'Microsoft.Azure.Workflows.Sdk'; -const codefulSdkPackageVersion = '1.0.0-preview.1'; -const lspSdkHashMarkerName = '.lspsdk-hash'; -const codefulSdkProjectHashMarkerName = '.logicapps-lspsdk-hash'; +import { localSettingsFileName } from '../../constants'; /** * Checks whether any workspace folder contains a codeful Logic Apps project. @@ -74,132 +67,6 @@ export const isCodefulProject = async (folderPath: string): Promise => return isCodefulNet8Csproj(csprojContent); }; -/** - * Invalidates only the project-local cache entry for the extension-shipped codeful SDK - * when the VSIX ships changed nupkg bits with the same package ID/version. - */ -export const invalidateCodefulSdkCacheIfNeeded = async (projectPath: string): Promise => { - if (!(await isCodefulProject(projectPath))) { - return false; - } - - const targetDirectory = getGlobalSetting(autoRuntimeDependenciesPathSettingKey); - const lspDirectoryPath = path.join(targetDirectory, lspDirectory); - const nugetConfigPath = path.join(projectPath, 'nuget.config'); - const installedSdkHashMarkerPath = path.join(targetDirectory, lspSdkHashMarkerName); - - if ( - !(await fse.pathExists(installedSdkHashMarkerPath)) || - !(await fse.pathExists(nugetConfigPath)) || - !(await codefulNugetConfigUsesExtensionSdkCache(nugetConfigPath, projectPath, lspDirectoryPath)) - ) { - return false; - } - - const installedSdkHash = (await fse.readFile(installedSdkHashMarkerPath, 'utf-8')).trim(); - if (!installedSdkHash) { - return false; - } - - const projectNugetFolder = path.join(projectPath, '.nuget'); - const projectSdkHashMarkerPath = path.join(projectNugetFolder, codefulSdkProjectHashMarkerName); - const projectSdkPackagePath = path.join(projectNugetFolder, 'packages', codefulSdkPackageId.toLowerCase(), codefulSdkPackageVersion); - const restoreNoOpCachePaths = [ - path.join(projectPath, 'obj', 'project.assets.json'), - path.join(projectPath, 'obj', 'project.nuget.cache'), - ]; - - if ((await readTrimmedFileIfExists(projectSdkHashMarkerPath)) === installedSdkHash) { - return false; - } - - if (await fse.pathExists(projectSdkPackagePath)) { - await fse.remove(projectSdkPackagePath); - ext.outputChannel.appendLog( - `Removed stale ${codefulSdkPackageId} ${codefulSdkPackageVersion} from project-local NuGet cache at ${projectSdkPackagePath}.` - ); - } - - await Promise.all(restoreNoOpCachePaths.map((cachePath) => removeIfExists(cachePath))); - await fse.ensureDir(projectNugetFolder); - await fse.writeFile(projectSdkHashMarkerPath, installedSdkHash); - return true; -}; - -async function removeIfExists(filePath: string): Promise { - if (await fse.pathExists(filePath)) { - await fse.remove(filePath); - } -} - -async function readTrimmedFileIfExists(filePath: string): Promise { - if (!(await fse.pathExists(filePath))) { - return undefined; - } - - try { - return (await fse.readFile(filePath, 'utf-8')).trim(); - } catch { - return undefined; - } -} - -async function codefulNugetConfigUsesExtensionSdkCache( - nugetConfigPath: string, - projectPath: string, - lspDirectoryPath: string -): Promise { - const nugetConfig = await fse.readFile(nugetConfigPath, 'utf-8'); - const globalPackagesFolder = getXmlAddValue(nugetConfig, 'config', 'globalPackagesFolder'); - const currentSource = getXmlAddValue(nugetConfig, 'packageSources', 'current'); - - return ( - globalPackagesFolder !== undefined && - normalizeNugetPath(projectPath, globalPackagesFolder) === normalizeNugetPath(projectPath, '.nuget\\packages') && - currentSource !== undefined && - normalizeNugetPath(projectPath, currentSource) === normalizeNugetPath(projectPath, lspDirectoryPath) - ); -} - -function getXmlAddValue(xml: string, sectionName: string, key: string): string | undefined { - const sectionMatch = stripXmlComments(xml).match(new RegExp(`<${sectionName}\\b[^>]*>([\\s\\S]*?)<\\/${sectionName}>`, 'i')); - const addElement = sectionMatch?.[1].match(new RegExp(`]*\\bkey=["']${escapeRegExp(key)}["'])[^>]*>`, 'i'))?.[0]; - return addElement?.match(/\bvalue=["']([^"']*)["']/i)?.[1]; -} - -function stripXmlComments(xml: string): string { - // This skips XML comments in local project files before regex parsing; it is not HTML output sanitization. - let result = ''; - let currentIndex = 0; - - while (currentIndex < xml.length) { - const commentStart = xml.indexOf('', commentStart + ''.length; - } - - return result; -} - -function normalizeNugetPath(projectPath: string, nugetPath: string): string { - const unquotedPath = nugetPath.trim().replace(/^["']|["']$/g, ''); - const resolvedPath = path.isAbsolute(unquotedPath) ? unquotedPath : path.resolve(projectPath, unquotedPath); - return path.normalize(resolvedPath).toLowerCase(); -} - -function escapeRegExp(value: string): string { - return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} - /** * Checks if a C# project file (.csproj) is configured for a codeful .NET 8 Azure Logic Apps workflow. * @@ -234,6 +101,28 @@ export interface CodefulCsprojBuildHookInfo { runsOnBuild: boolean; } +function stripXmlComments(xml: string): string { + let result = ''; + let currentIndex = 0; + + while (currentIndex < xml.length) { + const commentStart = xml.indexOf('', commentStart + ''.length; + } + + return result; +} + const findTargetAfterTargets = (csprojContent: string, targetName: string): string | null => { const stripped = stripXmlComments(csprojContent); const targetTagRegex = /]*?)\/?>/g; diff --git a/apps/vs-code-designer/src/app/utils/languageServerProtocol.ts b/apps/vs-code-designer/src/app/utils/languageServerProtocol.ts index 7907c65324a..8eae4549e64 100644 --- a/apps/vs-code-designer/src/app/utils/languageServerProtocol.ts +++ b/apps/vs-code-designer/src/app/utils/languageServerProtocol.ts @@ -1,7 +1,7 @@ import { callWithTelemetryAndErrorHandling } from '@microsoft/vscode-azext-utils'; import path from 'path'; import * as fse from 'fs-extra'; -import { assetsFolderName, lspDirectory } from '../../constants'; +import { assetsFolderName } from '../../constants'; import { ext } from '../../extensionVariables'; import { ensureRuntimeDependenciesPath } from './runtimeDependenciesPath'; import AdmZip from 'adm-zip'; @@ -9,10 +9,9 @@ import { createHash } from 'crypto'; const lspServerDirectoryName = 'LSPServer'; const lspServerHashMarkerName = '.lspserver-hash'; -const lspSdkHashMarkerName = '.lspsdk-hash'; -export async function installLSPSDK(): Promise { - await callWithTelemetryAndErrorHandling('azureLogicAppsStandard.installLSPSDK', async () => { +export async function installLSPServer(): Promise { + await callWithTelemetryAndErrorHandling('azureLogicAppsStandard.installLSPServer', async () => { const targetDirectory = await ensureRuntimeDependenciesPath(); // Check if LSPServer needs to be extracted or updated @@ -23,20 +22,9 @@ export async function installLSPSDK(): Promise { const serverZipHash = await getFileHash(serverZipFile); const shouldExtract = await shouldUpdateFromHash(serverZipHash, serverHashMarkerFile, lspServerDllPath); - // Check if SDK needs to be copied or updated - const lspDirectoryPath = path.join(targetDirectory, lspDirectory); - const sdkNupkgFile = path.join(__dirname, assetsFolderName, 'LSPServer', 'Microsoft.Azure.Workflows.Sdk.1.0.0-preview.1.nupkg'); - const sdkHashMarkerFile = path.join(targetDirectory, lspSdkHashMarkerName); - const destinationFile = path.join(lspDirectoryPath, path.basename(sdkNupkgFile)); - const sdkHash = await getFileHash(sdkNupkgFile); - - const shouldCopy = await shouldCopySdkFromHash(sdkHash, sdkHashMarkerFile, destinationFile); - - if (shouldExtract || shouldCopy) { + if (shouldExtract) { await stopLanguageClientForUpdate(); - } - if (shouldExtract) { try { if (await fse.pathExists(lspServerPath)) { await fse.remove(lspServerPath); @@ -56,19 +44,6 @@ export async function installLSPSDK(): Promise { throw new Error(`Error extracting LSP server: ${formatLockedFileError(error)}`); } } - - if (shouldCopy) { - try { - await fse.ensureDir(lspDirectoryPath); - - await fse.copyFile(sdkNupkgFile, destinationFile); - - await fse.writeFile(sdkHashMarkerFile, sdkHash); - await fse.remove(path.join(targetDirectory, '.lspsdk-version')); - } catch (error) { - throw new Error(`Error copying sdk: ${formatLockedFileError(error)}`); - } - } }); } @@ -96,18 +71,6 @@ async function shouldUpdateFromHash(sourceHash: string, hashMarkerFile: string, } } -async function shouldCopySdkFromHash(sourceHash: string, hashMarkerFile: string, destinationFile: string): Promise { - if (await shouldUpdateFromHash(sourceHash, hashMarkerFile, destinationFile)) { - return true; - } - - try { - return (await getFileHash(destinationFile)) !== sourceHash; - } catch { - return true; - } -} - async function getFileHash(filePath: string): Promise { const fileContent = await fse.readFile(filePath); return createHash('sha256').update(fileContent).digest('hex'); diff --git a/apps/vs-code-designer/src/app/utils/sdkResolution.ts b/apps/vs-code-designer/src/app/utils/sdkResolution.ts new file mode 100644 index 00000000000..c82d27a5d0c --- /dev/null +++ b/apps/vs-code-designer/src/app/utils/sdkResolution.ts @@ -0,0 +1,201 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as path from 'path'; +import * as fse from 'fs-extra'; +import { parseString } from 'xml2js'; +import { getDotNetCommand } from './dotnet/dotnet'; +import { executeCommand } from './funcCoreTools/cpUtils'; +import { ext } from '../../extensionVariables'; +import { localize } from '../../localize'; + +const SDK_PACKAGE_ID = 'Microsoft.Azure.Workflows.Sdk'; +const SDK_PACKAGE_ID_LOWER = 'microsoft.azure.workflows.sdk'; + +export interface SdkResolutionResult { + sdkNupkgPath: string; + version: string; +} + +/** + * Resolves the SDK nupkg path dynamically from the user's project. + * Parses the .csproj for the SDK PackageReference version, queries the NuGet + * global packages cache, and constructs the path to the cached nupkg. + * + * @param projectPath - The root folder of the Logic App project (containing the .csproj) + * @returns The resolved SDK nupkg path and version, or undefined if resolution fails + */ +export async function resolveSdkFromProject(projectPath: string): Promise { + try { + const csprojPath = await findCsprojFile(projectPath); + if (!csprojPath) { + ext.outputChannel.appendLog(localize('csprojFileNotFound', '[SDK Resolution] No .csproj file found in "{0}".', projectPath)); + return undefined; + } + + const sdkVersion = await parseSdkVersion(csprojPath); + if (!sdkVersion) { + ext.outputChannel.appendLog(localize('sdkVersionNotFound', '[SDK Resolution] No "{0}" PackageReference found in "{1}".', SDK_PACKAGE_ID, csprojPath)); + return undefined; + } + + const cacheRoot = await getNuGetGlobalPackagesPath(); + if (!cacheRoot) { + ext.outputChannel.appendLog(localize('nuGetCachePathNotFound', '[SDK Resolution] Failed to determine NuGet global packages path.')); + return undefined; + } + + const nupkgPath = path.join( + cacheRoot, + SDK_PACKAGE_ID_LOWER, + sdkVersion, + `${SDK_PACKAGE_ID_LOWER}.${sdkVersion}.nupkg` + ); + + if (await fse.pathExists(nupkgPath)) { + ext.outputChannel.appendLog(localize('sdkNupkgFound', '[SDK Resolution] Resolved SDK nupkg: "{0}".', nupkgPath)); + return { sdkNupkgPath: nupkgPath, version: sdkVersion }; + } + + // Fallback: check project-local .nuget/packages folder (for projects with globalPackagesFolder override) + const localNupkgPath = path.join( + projectPath, + '.nuget', + 'packages', + SDK_PACKAGE_ID_LOWER, + sdkVersion, + `${SDK_PACKAGE_ID_LOWER}.${sdkVersion}.nupkg` + ); + + if (await fse.pathExists(localNupkgPath)) { + ext.outputChannel.appendLog(localize('sdkNupkgFoundLocal', '[SDK Resolution] Resolved SDK nupkg from project-local cache: "{0}".', localNupkgPath)); + return { sdkNupkgPath: localNupkgPath, version: sdkVersion }; + } + + ext.outputChannel.appendLog(localize('sdkNupkgNotFound', '[SDK Resolution] SDK package not found at "{0}" or "{1}". Ensure \'dotnet restore\' has been run on the project.', nupkgPath, localNupkgPath)); + return undefined; + } catch (error) { + ext.outputChannel.appendLog(localize('sdkResolutionError', '[SDK Resolution] Unexpected error occurred: {0}', (error as Error).message)); + return undefined; + } +} + +/** + * Finds the first .csproj file in the given directory. + */ +async function findCsprojFile(folderPath: string): Promise { + try { + const files = await fse.readdir(folderPath); + const csprojFile = files.find((file) => file.endsWith('.csproj')); + return csprojFile ? path.join(folderPath, csprojFile) : undefined; + } catch { + return undefined; + } +} + +/** + * Parses the .csproj file to extract the Microsoft.Azure.Workflows.Sdk version + * using xml2js for robust XML handling. + */ +async function parseSdkVersion(csprojPath: string): Promise { + try { + const content = await fse.readFile(csprojPath, 'utf-8'); + const result = await parseCsprojXml(content); + if (!result) { + return undefined; + } + + const itemGroups: any[] = result.Project?.ItemGroup ?? []; + for (const group of itemGroups) { + const packageRefs: any[] = group.PackageReference ?? []; + for (const ref of packageRefs) { + const include = ref.$?.Include; + if (include?.toLowerCase() === SDK_PACKAGE_ID.toLowerCase()) { + const version = ref.$.Version?.trim(); + if (!version) { + continue; + } + if (version.startsWith('$(') || version.startsWith('@(')) { + ext.outputChannel.appendLog(localize('sdkVersionIsVariable', '[SDK Resolution] SDK version is an MSBuild variable "{0}", cannot resolve statically.', version)); + return undefined; + } + return version; + } + } + } + + return undefined; + } catch { + return undefined; + } +} + +function parseCsprojXml(content: string): Promise { + return new Promise((resolve) => { + parseString(content, (err, result) => { + resolve(err ? undefined : result); + }); + }); +} + +/** + * Queries the NuGet global packages cache root path using the dotnet CLI. + * Uses the configured dotnet binary path (respects azureLogicAppsStandard.dotnetBinaryPath setting). + * + * Output format: "global-packages: C:\Users\user\.nuget\packages\" + */ +async function getNuGetGlobalPackagesPath(): Promise { + try { + const dotnetCommand = getDotNetCommand(); + const output = await executeCommand(ext.outputChannel, undefined, dotnetCommand, 'nuget', 'locals', 'global-packages', '--list'); + + if (!output) { + return undefined; + } + + // Parse output: "global-packages: /path/to/packages/" + const prefix = 'global-packages:'; + const line = output.split('\n').find((l) => l.trim().toLowerCase().startsWith(prefix.toLowerCase())); + if (!line) { + ext.outputChannel.appendLog(localize('sdkUnexpectedNuGetOutput', '[SDK Resolution] Unexpected dotnet nuget locals output: "{0}".', output)); + return undefined; + } + + const cachePath = line.substring(line.indexOf(':') + 1).trim(); + if (!cachePath || !(await fse.pathExists(cachePath))) { + ext.outputChannel.appendLog(localize('sdkNuGetCachePathDoesNotExist', '[SDK Resolution] NuGet cache path does not exist: "{0}".', cachePath)); + return undefined; + } + + return cachePath; + } catch (error) { + ext.outputChannel.appendLog(localize('sdkResolutionError', '[SDK Resolution] Failed to query NuGet cache path: "{0}".', (error as Error).message)); + return getDefaultNuGetCachePath(); + } +} + +/** + * Fallback: returns the default NuGet cache path for the current OS. + * Used only if 'dotnet nuget locals' fails. + */ +async function getDefaultNuGetCachePath(): Promise { + // Check NUGET_PACKAGES environment variable first + const envPath = process.env.NUGET_PACKAGES; + if (envPath && (await fse.pathExists(envPath))) { + return envPath; + } + + // Default path by OS + const home = process.env.USERPROFILE || process.env.HOME; + if (!home) { + return undefined; + } + + const defaultPath = path.join(home, '.nuget', 'packages'); + if (await fse.pathExists(defaultPath)) { + return defaultPath; + } + + return undefined; +} diff --git a/apps/vs-code-designer/src/assets/CodefulProjectTemplate/ProgramFile b/apps/vs-code-designer/src/assets/CodefulProjectTemplate/ProgramFile index a0a3d5bfa30..e2b5f67d17f 100644 --- a/apps/vs-code-designer/src/assets/CodefulProjectTemplate/ProgramFile +++ b/apps/vs-code-designer/src/assets/CodefulProjectTemplate/ProgramFile @@ -28,9 +28,6 @@ namespace <%= logicAppNamespace %> }) .Build(); - // Build all workflows - <%= workflowBuilders %> - host.Run(); } } diff --git a/apps/vs-code-designer/src/assets/CodefulProjectTemplate/nuget b/apps/vs-code-designer/src/assets/CodefulProjectTemplate/nuget deleted file mode 100644 index d307f6efffc..00000000000 --- a/apps/vs-code-designer/src/assets/CodefulProjectTemplate/nuget +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - /> - - diff --git a/apps/vs-code-designer/src/assets/LSPServer/Microsoft.Azure.Workflows.Sdk.1.0.0-preview.1.nupkg b/apps/vs-code-designer/src/assets/LSPServer/Microsoft.Azure.Workflows.Sdk.1.0.0-preview.1.nupkg deleted file mode 100644 index ec4908d2730..00000000000 Binary files a/apps/vs-code-designer/src/assets/LSPServer/Microsoft.Azure.Workflows.Sdk.1.0.0-preview.1.nupkg and /dev/null differ diff --git a/apps/vs-code-designer/src/constants.ts b/apps/vs-code-designer/src/constants.ts index 6e491063cd5..b6d7274c508 100644 --- a/apps/vs-code-designer/src/constants.ts +++ b/apps/vs-code-designer/src/constants.ts @@ -36,7 +36,6 @@ export const customDirectory = 'custom'; export const mapsDirectory = 'Maps'; export const schemasDirectory = 'Schemas'; export const rulesDirectory = 'Rules'; -export const lspDirectory = 'LanguageServerLogicApps'; // Folder names export const designTimeDirectoryName = 'workflow-designtime';