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
48 changes: 27 additions & 21 deletions src/Aspire.Cli/Commands/NewCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ public NewCommand(

_languageOption = new Option<string?>("--language")
{
Description = NewCommandStrings.LanguageOptionDescription
Description = NewCommandStrings.LanguageOptionDescription,
Recursive = true
};
Comment thread
sebastienros marked this conversation as resolved.
Options.Add(_languageOption);

Expand Down Expand Up @@ -152,6 +153,9 @@ private static string GetLanguageDisplayName(string languageId)
KnownLanguageId.CSharp => KnownLanguageId.CSharpDisplayName,
KnownLanguageId.TypeScript => "TypeScript (Node.js)",
KnownLanguageId.Python => KnownLanguageId.PythonDisplayName,
KnownLanguageId.Go => KnownLanguageId.GoDisplayName,
KnownLanguageId.Java => KnownLanguageId.JavaDisplayName,
KnownLanguageId.Rust => KnownLanguageId.RustDisplayName,
_ => languageId
};
}
Expand Down Expand Up @@ -197,44 +201,32 @@ private async Task<string> PromptForAppHostLanguageAsync(IReadOnlyList<string> s
return (false, null);
}

await _configurationService.SetConfigurationAsync("language", normalizedExplicitLanguageId, isGlobal: false, cancellationToken);
return (true, normalizedExplicitLanguageId);
}

var configuredLanguageId = await _configurationService.GetConfigurationAsync("language", cancellationToken);
if (!string.IsNullOrWhiteSpace(configuredLanguageId))
{
var normalizedConfiguredLanguageId = NormalizeLanguageId(configuredLanguageId);
if (template.SelectableAppHostLanguages.Any(l => l.Equals(normalizedConfiguredLanguageId, StringComparison.OrdinalIgnoreCase)))
{
return (true, normalizedConfiguredLanguageId);
}
}

if (!_hostEnvironment.SupportsInteractiveInput)
{
return (true, NormalizeLanguageId(template.SelectableAppHostLanguages[0]));
}

var selectedLanguageId = await PromptForAppHostLanguageAsync(template.SelectableAppHostLanguages, cancellationToken);
await _configurationService.SetConfigurationAsync("language", selectedLanguageId, isGlobal: false, cancellationToken);
return (true, selectedLanguageId);
}

private ITemplate[] GetTemplatesForPrompt(ITemplate[] availableTemplates, ParseResult parseResult)
private ITemplate[] GetTemplatesForTemplateArgument(ITemplate[] availableTemplates, ParseResult parseResult)
{
var explicitLanguageId = ParseExplicitLanguageId(parseResult);
var templatesForPrompt = availableTemplates.ToList();
var templates = availableTemplates.ToList();

if (!string.IsNullOrWhiteSpace(explicitLanguageId))
{
templatesForPrompt = templatesForPrompt
templates = templates
.Where(t => t.SupportsLanguage(explicitLanguageId))
.ToList();
}

// Sort templates alphabetically by description, keeping empty templates at the end
templatesForPrompt.Sort((a, b) =>
templates.Sort((a, b) =>
{
var aIsEmpty = a.IsEmpty;
var bIsEmpty = b.IsEmpty;
Expand All @@ -247,7 +239,14 @@ private ITemplate[] GetTemplatesForPrompt(ITemplate[] availableTemplates, ParseR
return string.Compare(a.Description, b.Description, StringComparison.OrdinalIgnoreCase);
});

return templatesForPrompt.ToArray();
return templates.ToArray();
}

private ITemplate[] GetTemplatesForPrompt(ITemplate[] availableTemplates, ParseResult parseResult)
{
return GetTemplatesForTemplateArgument(availableTemplates, parseResult)
.Where(static t => t.ShowInPrompt)
.ToArray();
}

private async Task<ITemplate?> GetProjectTemplateAsync(ITemplate[] availableTemplates, ParseResult parseResult, CancellationToken cancellationToken)
Expand All @@ -267,8 +266,8 @@ private ITemplate[] GetTemplatesForPrompt(ITemplate[] availableTemplates, ParseR
return null;
}

var templatesForPrompt = GetTemplatesForPrompt(availableTemplates, parseResult);
if (templatesForPrompt.Length == 0)
var templatesForTemplateArgument = GetTemplatesForTemplateArgument(availableTemplates, parseResult);
if (templatesForTemplateArgument.Length == 0)
{
InteractionService.DisplayError("No templates are available for the current environment.");
return null;
Expand All @@ -277,11 +276,18 @@ private ITemplate[] GetTemplatesForPrompt(ITemplate[] availableTemplates, ParseR
if (!_hostEnvironment.SupportsInteractiveInput)
{
InteractionService.DisplayError(NewCommandStrings.NonInteractiveTemplateRequired);
var templateNames = string.Join(", ", templatesForPrompt.Select(t => t.Name));
var templateNames = string.Join(", ", templatesForTemplateArgument.Select(t => t.Name));
InteractionService.DisplaySubtleMessage(string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.NonInteractiveAvailableValues, templateNames));
throw new NonInteractiveException("template");
}

var templatesForPrompt = GetTemplatesForPrompt(availableTemplates, parseResult);
if (templatesForPrompt.Length == 0)
{
InteractionService.DisplayError("No templates are available for the current environment.");
return null;
}

var result = await _prompter.PromptForTemplateAsync(templatesForPrompt, cancellationToken);

// The prompt is cleared after selection.
Expand Down
5 changes: 4 additions & 1 deletion src/Aspire.Cli/Templating/CallbackTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@ internal class CallbackTemplate(
string? languageId = null,
Func<string, bool>? supportsLanguageCallback = null,
IReadOnlyList<string>? selectableAppHostLanguages = null,
bool isEmpty = false) : ITemplate
bool isEmpty = false,
bool showInPrompt = true) : ITemplate
{
public string Name => name;

public string Description => description;

public bool IsEmpty => isEmpty;

public bool ShowInPrompt => showInPrompt;

public TemplateRuntime Runtime => runtime;

public Func<string, string> PathDeriver => pathDeriverCallback;
Expand Down
71 changes: 62 additions & 9 deletions src/Aspire.Cli/Templating/CliTemplateFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ namespace Aspire.Cli.Templating;

internal sealed partial class CliTemplateFactory : ITemplateFactory
{
private static readonly string[] s_emptyAppHostLanguages =
[
KnownLanguageId.CSharp,
KnownLanguageId.TypeScript,
KnownLanguageId.Python,
KnownLanguageId.Go,
KnownLanguageId.Java,
KnownLanguageId.Rust
];

private static readonly HashSet<string> s_binaryTemplateExtensions =
[
".png",
Expand Down Expand Up @@ -105,17 +115,13 @@ private IEnumerable<ITemplate> GetTemplateDefinitions()

new CallbackTemplate(
KnownTemplateId.CSharpEmptyAppHost,
"Empty AppHost",
"Empty AppHost (Choose language...)",
projectName => $"./{projectName}",
cmd => AddOptionIfMissing(cmd, _localhostTldOption),
ApplyEmptyAppHostTemplateAsync,
runtime: TemplateRuntime.Cli,
supportsLanguageCallback: static languageId =>
languageId.Equals(KnownLanguageId.CSharp, StringComparison.OrdinalIgnoreCase) ||
languageId.Equals(KnownLanguageId.TypeScript, StringComparison.OrdinalIgnoreCase) ||
languageId.Equals(KnownLanguageId.TypeScriptAlias, StringComparison.OrdinalIgnoreCase) ||
languageId.Equals(KnownLanguageId.Python, StringComparison.OrdinalIgnoreCase),
selectableAppHostLanguages: [KnownLanguageId.CSharp, KnownLanguageId.TypeScript, KnownLanguageId.Python],
supportsLanguageCallback: IsSelectableEmptyAppHostLanguage,
selectableAppHostLanguages: GetSelectableEmptyAppHostLanguages(),
isEmpty: true),

new CallbackTemplate(
Expand All @@ -126,7 +132,19 @@ private IEnumerable<ITemplate> GetTemplateDefinitions()
ApplyEmptyAppHostTemplateAsync,
runtime: TemplateRuntime.Cli,
languageId: KnownLanguageId.TypeScript,
isEmpty: true),
isEmpty: true,
showInPrompt: false),

new CallbackTemplate(
KnownTemplateId.PythonEmptyAppHost,
"Empty (Python AppHost)",
projectName => $"./{projectName}",
cmd => AddOptionIfMissing(cmd, _localhostTldOption),
ApplyEmptyAppHostTemplateAsync,
runtime: TemplateRuntime.Cli,
languageId: KnownLanguageId.Python,
isEmpty: true,
showInPrompt: false),

new CallbackTemplate(
KnownTemplateId.JavaEmptyAppHost,
Expand All @@ -136,7 +154,30 @@ private IEnumerable<ITemplate> GetTemplateDefinitions()
ApplyEmptyAppHostTemplateAsync,
runtime: TemplateRuntime.Cli,
languageId: KnownLanguageId.Java,
isEmpty: true),
isEmpty: true,
showInPrompt: false),

new CallbackTemplate(
KnownTemplateId.GoEmptyAppHost,
"Empty (Go AppHost)",
projectName => $"./{projectName}",
cmd => AddOptionIfMissing(cmd, _localhostTldOption),
ApplyEmptyAppHostTemplateAsync,
runtime: TemplateRuntime.Cli,
languageId: KnownLanguageId.Go,
isEmpty: true,
showInPrompt: false),

new CallbackTemplate(
KnownTemplateId.RustEmptyAppHost,
"Empty (Rust AppHost)",
projectName => $"./{projectName}",
cmd => AddOptionIfMissing(cmd, _localhostTldOption),
ApplyEmptyAppHostTemplateAsync,
runtime: TemplateRuntime.Cli,
languageId: KnownLanguageId.Rust,
isEmpty: true,
showInPrompt: false),

new CallbackTemplate(
KnownTemplateId.PythonStarter,
Expand All @@ -155,6 +196,18 @@ private IEnumerable<ITemplate> GetTemplateDefinitions()
return templates.Where(IsTemplateAvailable);
}

private IReadOnlyList<string> GetSelectableEmptyAppHostLanguages()
{
return s_emptyAppHostLanguages
.Where(IsSelectableEmptyAppHostLanguage)
.ToArray();
}

private bool IsSelectableEmptyAppHostLanguage(string languageId)
{
return _languageDiscovery.GetLanguageById(new LanguageId(languageId)) is not null;
}

private bool IsTemplateAvailable(ITemplate template)
{
if (string.IsNullOrWhiteSpace(template.LanguageId))
Expand Down
5 changes: 5 additions & 0 deletions src/Aspire.Cli/Templating/ITemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ internal interface ITemplate
/// </summary>
bool IsEmpty { get; }

/// <summary>
/// Gets a value indicating whether this template should be shown in the interactive template picker.
/// </summary>
bool ShowInPrompt { get; }

/// <summary>
/// Gets the runtime model used to execute this template.
/// </summary>
Expand Down
15 changes: 15 additions & 0 deletions src/Aspire.Cli/Templating/KnownTemplateId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ internal static class KnownTemplateId
/// </summary>
public const string TypeScriptEmptyAppHost = "aspire-ts-empty";

/// <summary>
/// The template ID for the CLI Python empty AppHost template.
/// </summary>
public const string PythonEmptyAppHost = "aspire-py-empty";

/// <summary>
/// The template ID for the dotnet empty AppHost template.
/// </summary>
Expand All @@ -37,4 +42,14 @@ internal static class KnownTemplateId
/// The template ID for the Python starter template.
/// </summary>
public const string PythonStarter = "aspire-py-starter";

/// <summary>
/// The template ID for the CLI Go empty AppHost template.
/// </summary>
public const string GoEmptyAppHost = "aspire-go-empty";

/// <summary>
/// The template ID for the CLI Rust empty AppHost template.
/// </summary>
public const string RustEmptyAppHost = "aspire-rust-empty";
}
Loading
Loading