| File: Projects\ILanguageDiscovery.cs | Web Access |
| Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Aspire.Cli.Projects; /// <summary> /// A strongly-typed identifier for a programming language/runtime. /// </summary> /// <param name="Value">The language identifier value (e.g., "typescript/nodejs").</param> /// <remarks> /// Using a record struct ensures type safety and prevents accidental mixing of /// language IDs with other string parameters. /// </remarks> internal readonly record struct LanguageId(string Value) { /// <summary> /// Implicit conversion to string for convenience. /// </summary> public static implicit operator string(LanguageId id) => id.Value; /// <summary> /// Implicit conversion from string for convenience. /// </summary> public static implicit operator LanguageId(string value) => new(value); /// <inheritdoc /> public override string ToString() => Value; } /// <summary> /// Information about a supported language. /// </summary> /// <param name="LanguageId">The language identifier (e.g., "typescript/nodejs").</param> /// <param name="DisplayName">The display name for the language (e.g., "TypeScript (Node.js)").</param> /// <param name="PackageName">The NuGet package name for language support (e.g., "Aspire.Hosting.CodeGeneration.TypeScript").</param> /// <param name="DetectionPatterns">File patterns used to detect this language (e.g., ["apphost.ts"]).</param> /// <param name="CodeGenerator">The code generator name to use for this language (e.g., "TypeScript"). Must match ICodeGenerator.Language.</param> /// <param name="AppHostFileName">The default filename for the AppHost entry point (e.g., "apphost.mts").</param> /// <param name="IsExperimental">Whether this language is experimental and requires an additional per-language feature flag to be enabled.</param> /// <param name="PreserveUnchangedGeneratedFiles"> /// Whether regenerating identical content may leave the existing file, and therefore its last-write /// time, untouched. Only safe for languages whose toolchain reads the generated sources in place and /// decides what to rebuild from their timestamps, which is what makes skipping the rewrite the point: /// Java compiles <c>.aspire/modules</c> directly, so rewriting unchanged files forces javac to /// recompile the whole generated SDK on every launch. /// <para> /// It must stay <see langword="false" /> for languages that install the generated sources into an /// environment before running them. Python builds <c>.aspire/modules</c> into the virtual environment /// through uv, which decides whether to reuse its cached build from the source timestamps, so an /// unchanged file silently keeps a stale install rather than picking the regenerated SDK up. /// </para> /// </param> internal sealed record LanguageInfo( LanguageId LanguageId, string DisplayName, string PackageName, string[] DetectionPatterns, string CodeGenerator, string? AppHostFileName = null, bool IsExperimental = false, bool PreserveUnchangedGeneratedFiles = false) { /// <summary> /// The default folder path where generated code is placed for guest languages. /// </summary> internal static string GeneratedFolderName { get; } = Path.Combine(".aspire", "modules"); /// <summary> /// The legacy folder path where generated code was placed prior to consolidating /// generated artifacts under <c>.aspire/</c>. Used by the legacy TypeScript /// <c>apphost.ts</c> compatibility path which still imports from <c>./.modules/</c>. /// </summary> internal const string LegacyGeneratedFolderName = ".modules"; /// <summary> /// Maximum directory depth used when scanning the file system for language /// detection patterns. Keeps the scan fast in large workspaces while still /// finding AppHost files in typical nested project layouts. /// </summary> internal const int DetectionRecurseLimit = 5; /// <summary> /// Returns whether <paramref name="fileName"/> matches any of this /// language's <see cref="DetectionPatterns"/>. Supports exact names /// (e.g. <c>apphost.ts</c>) and wildcard extensions (e.g. <c>*.csproj</c>). /// </summary> internal bool MatchesFile(string fileName) { return DetectionPatterns.Any(p => MatchesPattern(fileName, p)); } /// <summary> /// Scans <paramref name="directory"/> (up to <see cref="DetectionRecurseLimit"/> /// levels deep) for any file matching this language's detection patterns. /// Uses <see cref="Utils.FileSystemHelper.FindFirstFile"/> so that glob /// patterns like <c>*.csproj</c> are expanded correctly — unlike a plain /// <see cref="File.Exists"/> call. /// </summary> /// <returns>The full path of the first matching file, or <c>null</c>.</returns> internal string? FindInDirectory(string directory) { return Utils.FileSystemHelper.FindFirstFile(directory, DetectionRecurseLimit, DetectionPatterns); } /// <summary> /// Checks whether <paramref name="fileName"/> matches a single detection /// pattern. Handles wildcard extension patterns (<c>*.csproj</c>) and /// exact file names (<c>apphost.ts</c>). /// </summary> internal static bool MatchesPattern(string fileName, string pattern) { if (pattern.StartsWith("*.", StringComparison.Ordinal)) { var extension = pattern[1..]; // ".csproj" return fileName.EndsWith(extension, StringComparison.OrdinalIgnoreCase); } return fileName.Equals(pattern, StringComparison.OrdinalIgnoreCase); } } /// <summary> /// Interface for discovering available languages. /// Implementations provide language metadata and detection capabilities. /// </summary> /// <remarks> /// This interface is designed to be async to support future implementations /// that may discover languages from external sources (NuGet, config files, etc.). /// </remarks> internal interface ILanguageDiscovery { /// <summary> /// Gets all available languages. /// </summary> /// <param name="cancellationToken">Cancellation token.</param> /// <returns>All available language information.</returns> Task<IEnumerable<LanguageInfo>> GetAvailableLanguagesAsync(CancellationToken cancellationToken = default); /// <summary> /// Gets the NuGet package name for a language. /// </summary> /// <param name="languageId">The language identifier (e.g., "typescript/nodejs").</param> /// <param name="cancellationToken">Cancellation token.</param> /// <returns>The package name, or null if the language is not found.</returns> Task<string?> GetPackageForLanguageAsync(LanguageId languageId, CancellationToken cancellationToken = default); /// <summary> /// Detects the language used in a directory by checking for known file patterns /// in the immediate directory only. Does not recurse into subdirectories. /// </summary> /// <param name="directory">The directory to check.</param> /// <param name="cancellationToken">Cancellation token.</param> /// <returns>The detected language ID, or null if no language was detected.</returns> Task<LanguageId?> DetectLanguageAsync(DirectoryInfo directory, CancellationToken cancellationToken = default); /// <summary> /// Detects the language used in a directory by recursively scanning for known /// file patterns up to <see cref="LanguageInfo.DetectionRecurseLimit"/> levels /// deep. Use this when a broader search is needed (e.g. <c>aspire doctor</c>), /// but be aware it is more expensive than <see cref="DetectLanguageAsync"/>. /// </summary> /// <param name="directory">The root directory to scan.</param> /// <param name="cancellationToken">Cancellation token.</param> /// <returns>The detected language ID, or null if no language was detected.</returns> Task<LanguageId?> DetectLanguageRecursiveAsync(DirectoryInfo directory, CancellationToken cancellationToken = default); /// <summary> /// Gets language information by its identifier. /// </summary> /// <param name="languageId">The language identifier.</param> /// <returns>The language info, or null if not found.</returns> LanguageInfo? GetLanguageById(LanguageId languageId); /// <summary> /// Gets language information by detecting from a file. /// </summary> /// <param name="file">The file to detect language from.</param> /// <returns>The language info, or null if not recognized.</returns> LanguageInfo? GetLanguageByFile(FileInfo file); }