|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Aspire.Cli.Configuration;
/// <summary>
/// Represents the .aspire/settings.json configuration file for polyglot app hosts.
/// This is the single source of truth for polyglot AppHost configuration,
/// analogous to .csproj for .NET AppHost projects.
/// </summary>
internal sealed class AspireJsonConfiguration
{
public const string SettingsFolder = ".aspire";
public const string FileName = "settings.json";
/// <summary>
/// The JSON Schema URL for this configuration file.
/// </summary>
[JsonPropertyName("$schema")]
[Description("The JSON Schema URL for this configuration file.")]
public string? Schema { get; set; }
/// <summary>
/// The path to the AppHost entry point file (e.g., "Program.cs", "app.ts").
/// Relative to the directory containing .aspire/settings.json.
/// </summary>
[JsonPropertyName("appHostPath")]
[LocalAspireJsonConfigurationProperty]
[Description("The path to the AppHost entry point file (e.g., \"Program.cs\", \"app.ts\"). Relative to the directory containing .aspire/settings.json.")]
public string? AppHostPath { get; set; }
/// <summary>
/// The language identifier for this AppHost (e.g., "typescript", "python").
/// Used to determine which runtime to use for execution.
/// </summary>
[JsonPropertyName("language")]
[Description("The language identifier for this AppHost (e.g., \"typescript\", \"python\"). Used to determine which runtime to use for execution.")]
public string? Language { get; set; }
/// <summary>
/// The Aspire channel to use for package resolution (e.g., "stable", "preview", "staging").
/// Used by aspire add to determine which NuGet feed to use.
/// </summary>
[JsonPropertyName("channel")]
[Description("The Aspire channel to use for package resolution (e.g., \"stable\", \"preview\", \"staging\"). Used by aspire add to determine which NuGet feed to use.")]
public string? Channel { get; set; }
/// <summary>
/// The Aspire SDK version used for this polyglot AppHost project.
/// Determines the version of Aspire.Hosting packages to use.
/// </summary>
[JsonPropertyName("sdkVersion")]
[Description("The Aspire SDK version used for this polyglot AppHost project. Determines the version of Aspire.Hosting packages to use.")]
public string? SdkVersion { get; set; }
/// <summary>
/// Package references as an object literal (like npm's package.json).
/// Key is package name, value is version.
/// </summary>
[JsonPropertyName("packages")]
[Description("Package references as an object literal (like npm's package.json). Key is package name, value is version.")]
public Dictionary<string, string>? Packages { get; set; }
/// <summary>
/// Feature flags for enabling/disabling experimental or optional features.
/// Key is feature name, value is enabled (true) or disabled (false).
/// </summary>
[JsonPropertyName("features")]
[JsonConverter(typeof(FlexibleBooleanDictionaryConverter))]
[Description("Feature flags for enabling/disabling experimental or optional features. Key is feature name, value is enabled (true) or disabled (false).")]
public Dictionary<string, bool>? Features { get; set; }
/// <summary>
/// Captures any additional properties not explicitly defined in this class.
/// This ensures settings like "features" are preserved when saving.
/// </summary>
[JsonExtensionData]
public Dictionary<string, JsonElement>? ExtensionData { get; set; }
/// <summary>
/// Gets the full path to the settings.json file.
/// </summary>
public static string GetFilePath(string directory)
{
return Path.Combine(directory, SettingsFolder, FileName);
}
/// <summary>
/// Loads the .aspire/settings.json configuration from the specified directory.
/// Returns null if the file doesn't exist.
/// </summary>
public static AspireJsonConfiguration? Load(string directory)
{
var filePath = GetFilePath(directory);
if (!File.Exists(filePath))
{
return null;
}
var json = File.ReadAllText(filePath);
return JsonSerializer.Deserialize(json, JsonSourceGenerationContext.Default.AspireJsonConfiguration);
}
/// <summary>
/// Loads the .aspire/settings.json configuration from the specified directory,
/// or creates a new one with the specified SDK version if it doesn't exist.
/// Ensures SdkVersion is always set.
/// </summary>
/// <param name="directory">The directory to load from.</param>
/// <param name="defaultSdkVersion">The default SDK version to use if not already set.</param>
/// <returns>The loaded or created configuration with SdkVersion guaranteed to be set.</returns>
public static AspireJsonConfiguration LoadOrCreate(string directory, string defaultSdkVersion)
{
var config = Load(directory) ?? new AspireJsonConfiguration();
config.SdkVersion ??= defaultSdkVersion;
return config;
}
/// <summary>
/// Saves the .aspire/settings.json configuration to the specified directory.
/// </summary>
public void Save(string directory)
{
var folderPath = Path.Combine(directory, SettingsFolder);
Directory.CreateDirectory(folderPath);
var filePath = Path.Combine(folderPath, FileName);
var json = JsonSerializer.Serialize(this, JsonSourceGenerationContext.Default.AspireJsonConfiguration);
File.WriteAllText(filePath, json);
}
/// <summary>
/// Adds a package reference, updating the version if it already exists.
/// </summary>
public void AddOrUpdatePackage(string packageId, string version)
{
Packages ??= [];
Packages[packageId] = version;
}
/// <summary>
/// Removes a package reference.
/// </summary>
public bool RemovePackage(string packageId)
{
if (Packages is null)
{
return false;
}
return Packages.Remove(packageId);
}
/// <summary>
/// Gets all package references including the base Aspire.Hosting packages.
/// Uses the SdkVersion for base packages.
/// </summary>
/// <returns>Enumerable of (PackageName, Version) tuples.</returns>
public IEnumerable<(string Name, string Version)> GetAllPackages()
{
var sdkVersion = SdkVersion ?? throw new InvalidOperationException("SdkVersion must be set before calling GetAllPackages. Use LoadOrCreate to ensure it's set.");
// Base packages always included
yield return ("Aspire.Hosting", sdkVersion);
yield return ("Aspire.Hosting.AppHost", sdkVersion);
// Additional packages from settings
if (Packages is not null)
{
foreach (var (packageName, version) in Packages)
{
// Skip base packages as they're already included
if (string.Equals(packageName, "Aspire.Hosting", StringComparison.OrdinalIgnoreCase) ||
string.Equals(packageName, "Aspire.Hosting.AppHost", StringComparison.OrdinalIgnoreCase))
{
continue;
}
yield return (packageName, version);
}
}
}
}
|