Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .azure-pipelines/1esmain.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ extends:
credscan:
suppressionsFile: $(Build.SourcesDirectory)/.azure-pipelines/compliance/CredScanSuppressions.json
codeql:
language: javascript # only build a codeql database for javascript, since the jsoncli pipeline handles csharp
language: javascript
# enabled: true # TODO: would like to enable only on scheduled builds but CodeQL cannot currently be disabled per https://eng.ms/docs/cloud-ai-platform/devdiv/one-engineering-system-1es/1es-docs/codeql/1es-codeql
pool:
name: VSEngSS-MicroBuild2022-1ES # Name of your hosted pool
Expand Down
3 changes: 0 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@
"editor.formatOnSave": true,
"editor.insertSpaces": true,
"editor.tabSize": 4,
"files.exclude": {
"tools/JsonCli/src/**": true
},
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true,
"search.exclude": {
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1,685 changes: 0 additions & 1,685 deletions resources/dotnetJsonCli/Microsoft.TemplateEngine.JsonCli.deps.json

This file was deleted.

Binary file not shown.

This file was deleted.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file removed resources/dotnetJsonCli/Newtonsoft.Json.dll
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@
*--------------------------------------------------------------------------------------------*/

import { type IActionContext } from '@microsoft/vscode-azext-utils';
import { composeArgs, withArg, withNamedArg } from '@microsoft/vscode-processutils';
import * as path from 'path';
import { type FuncVersion } from '../../../FuncVersion';
import { ext } from '../../../extensionVariables';
import { type FunctionTemplateBase } from '../../../templates/IFunctionTemplate';
import { executeDotnetTemplateCommand, validateDotnetInstalled } from '../../../templates/dotnet/executeDotnetTemplateCommand';
import { executeDotnetTemplateCreate, validateDotnetInstalled } from '../../../templates/dotnet/executeDotnetTemplateCommand';
import { nonNullProp } from '../../../utils/nonNull';
import { assertTemplateIsV1 } from '../../../utils/templateVersionUtils';
import { FunctionCreateStepBase } from '../FunctionCreateStepBase';
Expand All @@ -32,28 +31,26 @@ export class DotnetFunctionCreateStep extends FunctionCreateStepBase<IDotnetFunc

const functionName: string = nonNullProp(context, 'functionName');

// Build setting args dynamically
const settingArgs = template.userPromptedSettings
.filter(setting => getBindingSetting(context, setting) !== undefined)
.flatMap(setting => {
const value = getBindingSetting(context, setting);
return withNamedArg(`--arg:${setting.name}`, String(value), { shouldQuote: true })();
});

const args = composeArgs(
withNamedArg('--identity', template.id),
withNamedArg('--arg:name', functionName, { shouldQuote: true }),
withNamedArg('--arg:namespace', nonNullProp(context, 'namespace'), { shouldQuote: true }),
withArg(...settingArgs),
)();
// Build template args as a record
const templateArgs: Record<string, string> = {
name: functionName,
namespace: nonNullProp(context, 'namespace'),
};

for (const setting of template.userPromptedSettings) {
const value = getBindingSetting(context, setting);
if (value !== undefined) {
templateArgs[setting.name] = String(value);
}
}

const version: FuncVersion = nonNullProp(context, 'version');
let projectTemplateKey = context.projectTemplateKey;
if (!projectTemplateKey) {
const templateProvider = ext.templateProvider.get(context);
projectTemplateKey = await templateProvider.getProjectTemplateKey(context, context.projectPath, nonNullProp(context, 'language'), undefined, context.version, undefined);
}
await executeDotnetTemplateCommand(context, version, projectTemplateKey, context.projectPath, 'create', args);
await executeDotnetTemplateCreate(context, version, projectTemplateKey, context.projectPath, template.id, templateArgs);

return path.join(context.projectPath, functionName + getFileExtension(context));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ConnectionKey, ProjectLanguage, gitignoreFileName, hostFileName, localS
import { ext } from '../../../extensionVariables';
import { MismatchBehavior, setLocalAppSetting } from '../../../funcConfig/local.settings';
import { localize } from "../../../localize";
import { executeDotnetTemplateCommand, validateDotnetInstalled } from '../../../templates/dotnet/executeDotnetTemplateCommand';
import { executeDotnetTemplateCreate, validateDotnetInstalled } from '../../../templates/dotnet/executeDotnetTemplateCommand';
import { cpUtils } from '../../../utils/cpUtils';
import { nonNullProp } from '../../../utils/nonNull';
import { type IProjectWizardContext } from '../IProjectWizardContext';
Expand Down Expand Up @@ -58,14 +58,15 @@ export class DotnetProjectCreateStep extends ProjectCreateStepBase {
const functionsVersion: string = 'v' + majorVersion;
const projTemplateKey = nonNullProp(context, 'projectTemplateKey');

const args = composeArgs(
withNamedArg('--identity', identity),
withNamedArg('--arg:name', projectName, { shouldQuote: true }),
withNamedArg('--arg:AzureFunctionsVersion', functionsVersion),
withNamedArg('--arg:Framework', context.workerRuntime?.targetFramework, { shouldQuote: true }), // defaults to net6.0 if there is no targetFramework
)();
const templateArgs: Record<string, string> = {
name: projectName,
AzureFunctionsVersion: functionsVersion,
};
if (context.workerRuntime?.targetFramework) {
templateArgs.Framework = context.workerRuntime.targetFramework;
}

await executeDotnetTemplateCommand(context, version, projTemplateKey, context.projectPath, 'create', args);
await executeDotnetTemplateCreate(context, version, projTemplateKey, context.projectPath, identity, templateArgs);

await setLocalAppSetting(context, context.projectPath, ConnectionKey.Storage, '', MismatchBehavior.Overwrite);
}
Expand Down
2 changes: 1 addition & 1 deletion src/templates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,4 @@ In both cases, the templates are split into three parts: templates.json, binding

## .NET Templates

.NET templates are retrieved from the 'itemTemplates' and 'projectTemplates' properties in the CLI Feed. A single version of the Azure Functions runtime might support multiple versions of the .NET runtime, so we use the target framework and sdk from a user's `*.csproj` file to pick the matching templates. We then leverage the JsonCLI tool ('Microsoft.TemplateEngine.JsonCli') which provides a JSON-based way to interact with .NET templates. More information on that tool can be found in the [tools/JsonCli](https://github.com/microsoft/vscode-azurefunctions/tree/main/tools/JsonCli) folder at the root of this repo.
.NET templates are retrieved from the 'itemTemplates' and 'projectTemplates' properties in the CLI Feed. A single version of the Azure Functions runtime might support multiple versions of the .NET runtime, so we use the target framework and sdk from a user's `*.csproj` file to pick the matching templates. Templates are parsed directly from the nupkg files (which contain `.template.config/template.json` metadata) and instantiated using native `dotnet new` commands.
2 changes: 1 addition & 1 deletion src/templates/dotnet/DotnetTemplateProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ export class DotnetTemplateProvider extends TemplateProviderBase {
}

private async parseTemplates(context: IActionContext, projKey: string): Promise<ITemplates> {
this._rawTemplates = parseJson(await executeDotnetTemplateCommand(context, this.version, projKey, undefined, 'list'));
this._rawTemplates = parseJson(await executeDotnetTemplateCommand(context, this.version, projKey));
return parseDotnetTemplates(this._rawTemplates, this.version);
}

Expand Down
132 changes: 113 additions & 19 deletions src/templates/dotnet/executeDotnetTemplateCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,132 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { type IActionContext } from '@microsoft/vscode-azext-utils';
import { composeArgs, withArg, withNamedArg, withQuotedArg, type CommandLineArgs } from '@microsoft/vscode-processutils';
import { randomUtils, type IActionContext } from '@microsoft/vscode-azext-utils';
import { composeArgs, withArg, withNamedArg, withQuotedArg } from '@microsoft/vscode-processutils';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { coerce as semVerCoerce, type SemVer } from 'semver';
import { type FuncVersion } from '../../FuncVersion';
import { ext } from "../../extensionVariables";
import { localize } from '../../localize';
import { cpUtils } from "../../utils/cpUtils";
import { findShortNameByIdentity, parseTemplatesFromNupkg } from './parseNupkgTemplates';

export async function executeDotnetTemplateCommand(context: IActionContext, version: FuncVersion, projTemplateKey: string, workingDirectory: string | undefined, operation: 'list' | 'create', additionalArgs?: CommandLineArgs): Promise<string> {
const jsonDllPath: string = ext.context.asAbsolutePath(path.join('resources', 'dotnetJsonCli', 'Microsoft.TemplateEngine.JsonCli.dll'));

const args = composeArgs(
withNamedArg('--roll-forward', 'Major'),
withQuotedArg(jsonDllPath),
withNamedArg('--templateDir', getDotnetTemplateDir(context, version, projTemplateKey), { shouldQuote: true }),
withNamedArg('--operation', operation),
withArg(...(additionalArgs ?? [])),
)();
return await cpUtils.executeCommand(
undefined,
workingDirectory,
'dotnet',
args);
const itemNupkgFileName = 'item.nupkg';
const projectNupkgFileName = 'project.nupkg';

/**
* Lists templates by parsing nupkg files directly (no longer uses the JsonCli DLL).
*/
export async function executeDotnetTemplateCommand(context: IActionContext, version: FuncVersion, projTemplateKey: string): Promise<string> {
const templateDir = getDotnetTemplateDir(context, version, projTemplateKey);
return await listDotnetTemplates(templateDir);
}

/**
* Lists all templates from the item.nupkg and project.nupkg in the template directory
* by parsing the `.template.config/template.json` files directly from the nupkg archives.
*/
async function listDotnetTemplates(templateDir: string): Promise<string> {
const itemNupkg = path.join(templateDir, itemNupkgFileName);
const projectNupkg = path.join(templateDir, projectNupkgFileName);

const templates: object[] = [];

for (const nupkgPath of [itemNupkg, projectNupkg]) {
try {
await fs.promises.access(nupkgPath);
templates.push(...await parseTemplatesFromNupkg(nupkgPath));
} catch {
// nupkg doesn't exist, skip
}
}

return JSON.stringify(templates);
}

/**
* Creates a function or project from a .NET template using native `dotnet new` commands.
* Uses an isolated DOTNET_CLI_HOME to avoid polluting the user's global template installation.
*/
export async function executeDotnetTemplateCreate(
context: IActionContext,
version: FuncVersion,
projTemplateKey: string,
workingDirectory: string | undefined,
identity: string,
templateArgs: Record<string, string>,
): Promise<void> {
Comment on lines +55 to +62
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New behavior (nupkg template parsing + dotnet new install/dotnet new) is not directly covered by tests. Consider adding unit/integration tests that validate: (1) parsing of .template.config/template.json from the shipped backup nupkgs, including choice/boolean symbols and multi-shortName cases; and (2) executeDotnetTemplateCreate runs with an isolated template home and succeeds in generating output in a temp directory.

Copilot uses AI. Check for mistakes.
const templateDir = getDotnetTemplateDir(context, version, projTemplateKey);
const itemNupkg = path.join(templateDir, itemNupkgFileName);
const projectNupkg = path.join(templateDir, projectNupkgFileName);

// Collect existing nupkg paths
const nupkgPaths: string[] = [];
for (const p of [itemNupkg, projectNupkg]) {
try {
await fs.promises.access(p);
nupkgPaths.push(p);
} catch {
// doesn't exist, skip
}
}

// Find the shortName for the given template identity
const shortName = await findShortNameByIdentity(nupkgPaths, identity);

// Use an isolated DOTNET_CLI_HOME so template installation doesn't affect the user's global state
// This is how the JSON CLI tool operated
const tempCliHome = path.join(os.tmpdir(), `azfunc-dotnet-home-${randomUtils.getRandomHexString()}`);
const prevDotnetCliHome = process.env.DOTNET_CLI_HOME;

try {
process.env.DOTNET_CLI_HOME = tempCliHome;

Comment on lines +81 to +88
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

executeDotnetTemplateCreate mutates process.env.DOTNET_CLI_HOME globally. If multiple template operations (or other .NET operations in the extension) run concurrently, this can lead to race conditions and cross-contamination of state. Prefer passing an env override per spawned process (and/or extending cpUtils.executeCommand to accept env) rather than mutating the global environment.

Copilot uses AI. Check for mistakes.
// Install template packages
for (const nupkgPath of nupkgPaths) {
await cpUtils.executeCommand(
undefined,
undefined,
'dotnet',
composeArgs(withArg('new', 'install'), withQuotedArg(nupkgPath))(),
);
}

// Build dotnet new args: dotnet new <shortName> --<param> <value> ...
const createArgs = composeArgs(
withArg('new', shortName),
...Object.entries(templateArgs)
.filter(([, value]) => value !== undefined && value !== '')
.map(([key, value]) => withNamedArg(`--${key}`, value, { shouldQuote: true })),
)();

await cpUtils.executeCommand(
undefined,
workingDirectory,
'dotnet',
createArgs,
);
} finally {
// Restore DOTNET_CLI_HOME
if (prevDotnetCliHome !== undefined) {
process.env.DOTNET_CLI_HOME = prevDotnetCliHome;
} else {
delete process.env.DOTNET_CLI_HOME;
}

// Clean up isolated home directory
await fs.promises.rm(tempCliHome, { recursive: true, force: true }).catch(() => { /* best-effort cleanup */ });
}
}

export function getDotnetItemTemplatePath(context: IActionContext, version: FuncVersion, projTemplateKey: string): string {
return path.join(getDotnetTemplateDir(context, version, projTemplateKey), 'item.nupkg');
return path.join(getDotnetTemplateDir(context, version, projTemplateKey), itemNupkgFileName);
}

export function getDotnetProjectTemplatePath(context: IActionContext, version: FuncVersion, projTemplateKey: string): string {
return path.join(getDotnetTemplateDir(context, version, projTemplateKey), 'project.nupkg');
return path.join(getDotnetTemplateDir(context, version, projTemplateKey), projectNupkgFileName);
}

export function getDotnetTemplateDir(context: IActionContext, version: FuncVersion, projTemplateKey: string): string {
Expand Down
Loading
Loading