Skip to content

Commit 3059380

Browse files
feat(vscode): add .NET 10 support for custom code projects (#8895)
* feat(vscode): add .NET 10 support for custom code projects Add .NET 10 as a selectable target framework across the VS Code extension and webview for custom code project creation, detection, and debugging. - Add TargetFramework.Net10 enum value and DotnetVersion.net10 constant - Create FunctionsFileNet10 and FunctionsProjNet10New template files - Add net10.0 backup templates for dotnet template fallback - Expose .NET 10 in extension quick-pick and webview dropdown selectors - Add DOTNET_10 and DOTNET_10_DESCRIPTION localization strings - Wire Net10 into template mappings in functionAppFilesStep and CreateFunctionAppFiles with shared usesPublishFolderProperty helper - Extract getCustomCodeRuntime helper for coreclr/clr runtime selection - Add isCustomCodeNet10Csproj detection and getCustomCodeTargetFramework dispatcher in customCodeUtils with usesLogicAppFolderToPublish helper - Reprioritize .NET version detection to prefer 10 over 8 - Update codeful workflow creation to default to .NET 10 - Add DotnetVersion.net10 to SDK version mapping in updateBuildFile - Add unit tests for debug config, custom code detection, VSCode contents generation, framework version probing, and review step rendering Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(vscode): restore .NET 8 test coverage and fix import regression - Restore original .NET 8 test cases in executeDotnetTemplateCommand tests that were incorrectly replaced instead of supplemented with .NET 10 cases - Add .NET 10 binaries test case alongside existing .NET 8 one - Fix runtime import for ProjectType/ProjectLanguage/latestGAVersion in CreateLogicAppVSCodeContents.ts (was incorrectly collapsed to type-only) - Restore .NET 6 SDK version mapping to 4.1.3 in updateBuildFile.ts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(vscode): add dotnetJsonCli net10.0 asset and fix import regression - Copy dotnetJsonCli/net8.0 to dotnetJsonCli/net10.0 so template operations work when getFramework returns net10.0 - Restore value imports for ProjectType, ProjectLanguage, latestGAVersion in CreateLogicAppVSCodeContents.ts (biome useImportType kept collapsing them to type-only imports which breaks runtime) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(vscode): map net10.0 to net8.0 dotnetJsonCli instead of duplicating assets Remove the duplicated dotnetJsonCli/net10.0 folder and add a getJsonCliFramework() mapper that falls back to net8.0 for unsupported frameworks. The JsonCli DLLs are framework-agnostic and forward-compatible, so net10.0 can reuse net8.0 binaries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(vscode): improve coverage for .NET 10 changes - Add explicit getCustomCodeRuntime tests for Net8, Net10, and NetFx - Add updateFunctionsSDKVersion tests for .NET 8 (4.5.0) and .NET 10 (4.5.0) - Export getJsonCliFramework and add tests for framework-to-asset mapping including net10.0 → net8.0 fallback behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(vscode): deduplicate usesPublishFolderProperty and guard null SDK version - Extract usesPublishFolderProperty into shared debug.ts utility, removing duplicated private methods from CreateFunctionAppFiles and FunctionAppFilesStep - Guard against null getLocalDotNetVersionFromBinaries result in switchToDotnetProject to prevent writing invalid global.json; log a warning when SDK version cannot be determined - Add unit tests for usesPublishFolderProperty covering all project types and target frameworks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(vscode): add coverage for .NET 10 changes across 5 uncovered files Add unit tests for the NET10 branch changes in files that had 0% coverage: - functionFileStep: Net10 template mapping - functionAppFilesStep: Net10 cs/csproj template mappings - targetFrameworkStep: Net10 picker option and shouldPrompt logic - dotnet.ts: FuncVersion.v4 defaults to net10.0 - dotNetFrameworkStep: Net10 dropdown rendering, selection dispatch Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(vscode): update .NET 10 template and rename project templates - Update FunctionsProjNet10 with correct .NET 10 packages and build targets - Change TFM from net10 to net10.0 across codebase - Rename FunctionsProjNet8New → FunctionsProjNet8 - Rename FunctionsProjNet10New → FunctionsProjNet10 - Delete unused legacy FunctionsProjNet8 template - Update TargetFramework.Net10 enum to 'net10.0' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(vscode): add Program.cs generation for .NET 10 isolated worker model - Create ProgramFileNet10 template with namespace placeholder - Update CreateFunctionAppFiles and FunctionAppFilesStep to generate Program.cs for .NET 10 custom code projects - Add 8 tests covering Program.cs creation, namespace replacement, and skipping for Net8/NetFx/rulesEngine projects Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(vscode): extract shared function project file utilities - Extract duplicated template maps and file-creation methods into shared functionProjectFiles.ts utility module - Replace hardcoded framework strings with TargetFramework constants in dotNetFrameworkStep and reviewCreateStep - Remove brittle (step as any) casts in tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(vscode): add newline delimiter between version sources in getFramework Version sources were concatenated with no delimiter, causing the multiline regex to miss versions when outputs lacked trailing newlines. For example, '8.0.100' + '10.0.200 [/sdk]' became '8.0.10010.0.200' which matched neither version. Using join('\n') ensures each source starts on its own line so the Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 55400fe commit 3059380

File tree

37 files changed

+1559
-395
lines changed

37 files changed

+1559
-395
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { TargetFramework } from '@microsoft/vscode-extension-logic-apps';
3+
4+
vi.mock('fs-extra');
5+
vi.mock('vscode');
6+
vi.mock('../../../../../extensionVariables', () => ({
7+
ext: { outputChannel: { appendLog: vi.fn() } },
8+
}));
9+
vi.mock('../../../../../localize', () => ({
10+
localize: (_key: string, msg: string) => msg,
11+
}));
12+
13+
import { FunctionFileStep } from '../functionFileStep';
14+
15+
describe('FunctionFileStep', () => {
16+
describe('csTemplateFileName mapping', () => {
17+
it('should map Net10 to FunctionsFileNet10', () => {
18+
const step = new FunctionFileStep();
19+
const mapping = (step as any).csTemplateFileName;
20+
expect(mapping[TargetFramework.Net10]).toBe('FunctionsFileNet10');
21+
});
22+
23+
it('should preserve Net8 mapping', () => {
24+
const step = new FunctionFileStep();
25+
const mapping = (step as any).csTemplateFileName;
26+
expect(mapping[TargetFramework.Net8]).toBe('FunctionsFileNet8');
27+
});
28+
29+
it('should preserve NetFx mapping', () => {
30+
const step = new FunctionFileStep();
31+
const mapping = (step as any).csTemplateFileName;
32+
expect(mapping[TargetFramework.NetFx]).toBe('FunctionsFileNetFx');
33+
});
34+
35+
it('should contain exactly three framework entries', () => {
36+
const step = new FunctionFileStep();
37+
const mapping = (step as any).csTemplateFileName;
38+
expect(Object.keys(mapping)).toHaveLength(3);
39+
});
40+
});
41+
42+
describe('shouldPrompt', () => {
43+
it('should always return true', () => {
44+
const step = new FunctionFileStep();
45+
expect(step.shouldPrompt()).toBe(true);
46+
});
47+
});
48+
});

apps/vs-code-designer/src/app/commands/createCustomCodeFunction/createCustomCodeFunctionSteps/functionFileStep.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export class FunctionFileStep extends AzureWizardPromptStep<IProjectWizardContex
2121
private csTemplateFileName = {
2222
[TargetFramework.NetFx]: 'FunctionsFileNetFx',
2323
[TargetFramework.Net8]: 'FunctionsFileNet8',
24+
[TargetFramework.Net10]: 'FunctionsFileNet10',
2425
};
2526

2627
/**

apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/CreateFunctionAppFiles.ts

Lines changed: 13 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -21,137 +21,38 @@ import { getDebugConfigs, updateDebugConfigs } from '../../../utils/vsCodeConfig
2121
import { getContainingWorkspace, isMultiRootWorkspace } from '../../../utils/workspace';
2222
import { localize } from '../../../../localize';
2323
import * as vscode from 'vscode';
24+
import { getCustomCodeRuntime } from '../../../utils/debug';
25+
import { createCsFile, createProgramFile, createRulesFiles, createCsprojFile } from '../../../utils/functionProjectFiles';
26+
2427
/**
2528
* This class represents a prompt step that allows the user to set up an Azure Function project.
2629
*/
2730
export class CreateFunctionAppFiles {
2831
// Hide the step count in the wizard UI
2932
public hideStepCount = true;
3033

31-
private csTemplateFileName = {
32-
[TargetFramework.NetFx]: 'FunctionsFileNetFx',
33-
[TargetFramework.Net8]: 'FunctionsFileNet8',
34-
[ProjectType.rulesEngine]: 'RulesFunctionsFile',
35-
};
36-
37-
private csprojTemplateFileName = {
38-
[TargetFramework.NetFx]: 'FunctionsProjNetFx',
39-
[TargetFramework.Net8]: 'FunctionsProjNet8New',
40-
[ProjectType.rulesEngine]: 'RulesFunctionsProj',
41-
};
42-
43-
private templateFolderName = {
44-
[ProjectType.customCode]: 'FunctionProjectTemplate',
45-
[ProjectType.rulesEngine]: 'RuleSetProjectTemplate',
46-
};
47-
4834
/**
49-
* Prompts the user to set up an Azure Function project.
35+
* Sets up an Azure Function project by creating all necessary files.
5036
* @param context The project wizard context.
5137
*/
5238
public async setup(context: IProjectWizardContext): Promise<void> {
53-
// Set the functionAppName and namespaceName properties from the context wizard
54-
const functionAppName = context.functionAppName;
55-
const namespace = context.functionAppNamespace;
56-
const targetFramework = context.targetFramework;
39+
const { functionAppName, functionAppNamespace: namespace, targetFramework, projectType } = context;
5740
const logicAppName = context.logicAppName || 'LogicApp';
58-
// const funcVersion = context.version ?? (await tryGetLocalFuncVersion());
59-
60-
// Define the functions folder path using the context property of the wizard
6141
const functionFolderPath = path.join(context.workspacePath, context.functionFolderName);
62-
await fs.ensureDir(functionFolderPath);
63-
64-
// Define the type of project in the workspace
65-
const projectType = context.projectType;
42+
const assetsPath = path.join(__dirname, assetsFolderName);
6643

67-
// Create the .cs file inside the functions folder
68-
await this.createCsFile(functionFolderPath, functionAppName, namespace, projectType, targetFramework);
44+
await fs.ensureDir(functionFolderPath);
45+
await createCsFile(assetsPath, functionFolderPath, functionAppName, namespace, projectType, targetFramework);
46+
await createProgramFile(assetsPath, functionFolderPath, namespace, projectType, targetFramework);
6947

70-
// Create the .cs files inside the functions folders for rule code projects
7148
if (projectType === ProjectType.rulesEngine) {
72-
await this.createRulesFiles(functionFolderPath);
49+
await createRulesFiles(assetsPath, functionFolderPath);
7350
}
7451

75-
// Create the .csproj file inside the functions folder
76-
await this.createCsprojFile(functionFolderPath, functionAppName, logicAppName, projectType, targetFramework);
77-
78-
// Generate the Visual Studio Code configuration files in the specified folder.
52+
await createCsprojFile(assetsPath, functionFolderPath, functionAppName, logicAppName, projectType, targetFramework);
7953
await this.createVscodeConfigFiles(functionFolderPath, targetFramework);
8054
}
8155

82-
/**
83-
* Creates the .cs file inside the functions folder.
84-
* @param functionFolderPath - The path to the functions folder.
85-
* @param methodName - The name of the method.
86-
* @param namespace - The name of the namespace.
87-
* @param projectType - The workspace projet type.
88-
* @param targetFramework - The target framework.
89-
*/
90-
private async createCsFile(
91-
functionFolderPath: string,
92-
methodName: string,
93-
namespace: string,
94-
projectType: ProjectType,
95-
targetFramework: TargetFramework
96-
): Promise<void> {
97-
const templateFile =
98-
projectType === ProjectType.rulesEngine ? this.csTemplateFileName[ProjectType.rulesEngine] : this.csTemplateFileName[targetFramework];
99-
const templatePath = path.join(__dirname, assetsFolderName, this.templateFolderName[projectType], templateFile);
100-
const templateContent = await fs.readFile(templatePath, 'utf-8');
101-
102-
const csFilePath = path.join(functionFolderPath, `${methodName}.cs`);
103-
const csFileContent = templateContent.replace(/<%= methodName %>/g, methodName).replace(/<%= namespace %>/g, namespace);
104-
await fs.writeFile(csFilePath, csFileContent);
105-
}
106-
107-
/**
108-
* Creates the rules files for the project.
109-
* @param {string} functionFolderPath - The path of the function folder.
110-
* @returns A promise that resolves when the rules files are created.
111-
*/
112-
private async createRulesFiles(functionFolderPath: string): Promise<void> {
113-
const csTemplatePath = path.join(__dirname, assetsFolderName, 'RuleSetProjectTemplate', 'ContosoPurchase');
114-
const csRuleSetPath = path.join(functionFolderPath, 'ContosoPurchase.cs');
115-
await fs.copyFile(csTemplatePath, csRuleSetPath);
116-
}
117-
118-
/**
119-
* Creates a .csproj file for a specific Azure Function.
120-
* @param functionFolderPath - The path to the folder where the .csproj file will be created.
121-
* @param methodName - The name of the Azure Function.
122-
* @param projectType - The workspace projet type.
123-
* @param targetFramework - The target framework.
124-
*/
125-
private async createCsprojFile(
126-
functionFolderPath: string,
127-
methodName: string,
128-
logicAppName: string,
129-
projectType: ProjectType,
130-
targetFramework: TargetFramework
131-
): Promise<void> {
132-
const templateFile =
133-
projectType === ProjectType.rulesEngine
134-
? this.csprojTemplateFileName[ProjectType.rulesEngine]
135-
: this.csprojTemplateFileName[targetFramework];
136-
const templatePath = path.join(__dirname, assetsFolderName, this.templateFolderName[projectType], templateFile);
137-
const templateContent = await fs.readFile(templatePath, 'utf-8');
138-
139-
const csprojFilePath = path.join(functionFolderPath, `${methodName}.csproj`);
140-
let csprojFileContent: string;
141-
if (targetFramework === TargetFramework.Net8 && projectType === ProjectType.customCode) {
142-
csprojFileContent = templateContent.replace(
143-
/<LogicAppFolderToPublish>\$\(MSBuildProjectDirectory\)\\..\\LogicApp<\/LogicAppFolderToPublish>/g,
144-
`<LogicAppFolderToPublish>$(MSBuildProjectDirectory)\\..\\${logicAppName}</LogicAppFolderToPublish>`
145-
);
146-
} else {
147-
csprojFileContent = templateContent.replace(
148-
/<LogicAppFolder>LogicApp<\/LogicAppFolder>/g,
149-
`<LogicAppFolder>${logicAppName}</LogicAppFolder>`
150-
);
151-
}
152-
await fs.writeFile(csprojFilePath, csprojFileContent);
153-
}
154-
15556
/**
15657
* Creates the Visual Studio Code configuration files in the .vscode folder of the specified functions app.
15758
* @param functionFolderPath The path to the functions folder.
@@ -232,7 +133,7 @@ export class CreateFunctionAppFiles {
232133
if (debugConfig.type === 'logicapp') {
233134
return {
234135
...debugConfig,
235-
customCodeRuntime: targetFramework === TargetFramework.Net8 ? 'coreclr' : 'clr',
136+
customCodeRuntime: getCustomCodeRuntime(targetFramework),
236137
isCodeless: true,
237138
};
238139
}
@@ -244,7 +145,7 @@ export class CreateFunctionAppFiles {
244145
type: 'logicapp',
245146
request: 'launch',
246147
funcRuntime: funcVersion === FuncVersion.v1 ? 'clr' : 'coreclr',
247-
customCodeRuntime: targetFramework === TargetFramework.Net8 ? 'coreclr' : 'clr',
148+
customCodeRuntime: getCustomCodeRuntime(targetFramework),
248149
isCodeless: true,
249150
},
250151
...debugConfigs.filter(

apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/CreateLogicAppVSCodeContents.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { latestGAVersion, ProjectLanguage, ProjectType, TargetFramework } from '@microsoft/vscode-extension-logic-apps';
2-
import type { ILaunchJson, ISettingToAdd, IWebviewProjectContext } from '@microsoft/vscode-extension-logic-apps';
1+
import { latestGAVersion, ProjectLanguage, ProjectType } from '@microsoft/vscode-extension-logic-apps';
2+
import type { ILaunchJson, ISettingToAdd, IWebviewProjectContext, TargetFramework } from '@microsoft/vscode-extension-logic-apps';
33
import {
44
assetsFolderName,
55
containerTemplatesFolderName,
@@ -24,20 +24,22 @@ import { confirmEditJsonFile } from '../../../utils/fs';
2424
import type { IActionContext } from '@microsoft/vscode-azext-utils';
2525
import { localize } from '../../../../localize';
2626
import { ext } from '../../../../extensionVariables';
27+
import { getCustomCodeRuntime } from '../../../utils/debug';
2728
import { isDebugConfigEqual } from '../../../utils/vsCodeConfig/launch';
2829

2930
export async function writeSettingsJson(
3031
context: IWebviewProjectContext,
3132
additionalSettings: ISettingToAdd[],
3233
vscodePath: string
3334
): Promise<void> {
34-
const settings: ISettingToAdd[] = additionalSettings.concat(
35+
const settings: ISettingToAdd[] = [
36+
...additionalSettings,
3537
{ key: projectLanguageSetting, value: ProjectLanguage.JavaScript },
3638
{ key: funcVersionSetting, value: latestGAVersion },
3739
// We want the terminal to open after F5, not the debug console because HTTP triggers are printed in the terminal.
3840
{ prefix: 'debug', key: 'internalConsoleOptions', value: 'neverOpen' },
39-
{ prefix: 'azureFunctions', key: 'suppressProject', value: true }
40-
);
41+
{ prefix: 'azureFunctions', key: 'suppressProject', value: true },
42+
];
4143

4244
const settingsJsonPath: string = path.join(vscodePath, settingsFileName);
4345
await confirmEditJsonFile(context, settingsJsonPath, (data: Record<string, any>): Record<string, any> => {
@@ -76,7 +78,7 @@ export function getDebugConfiguration(logicAppName: string, customCodeTargetFram
7678
type: 'logicapp',
7779
request: 'launch',
7880
funcRuntime: 'coreclr',
79-
customCodeRuntime: customCodeTargetFramework === TargetFramework.Net8 ? 'coreclr' : 'clr',
81+
customCodeRuntime: getCustomCodeRuntime(customCodeTargetFramework),
8082
isCodeless: true,
8183
};
8284
}
@@ -107,12 +109,8 @@ export async function writeLaunchJson(
107109
}
108110

109111
export function insertLaunchConfig(existingConfigs: DebugConfiguration[] | undefined, newConfig: DebugConfiguration): DebugConfiguration[] {
110-
// tslint:disable-next-line: strict-boolean-expressions
111-
existingConfigs = existingConfigs || [];
112-
// Remove configs that match the one we're about to add
113-
existingConfigs = existingConfigs.filter((l1) => !isDebugConfigEqual(l1, newConfig));
114-
existingConfigs.push(newConfig);
115-
return existingConfigs;
112+
const configs = (existingConfigs ?? []).filter((existingConfig) => !isDebugConfigEqual(existingConfig, newConfig));
113+
return [...configs, newConfig];
116114
}
117115

118116
export async function createLogicAppVsCodeContents(

0 commit comments

Comments
 (0)