File: Utils\EnvironmentChecker\TypeScriptAppHostToolingCheck.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.
 
using System.Text.Json.Nodes;
using Aspire.Cli.Projects;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Cli.Utils.EnvironmentChecker;
 
internal sealed class TypeScriptAppHostToolingCheck : IEnvironmentCheck
{
    internal const string YarnClassicCheckName = "typescript-apphost-yarn-classic";
    internal const string ToolsCheckName = "typescript-apphost-tools";
 
    private readonly IProjectLocator _projectLocator;
    private readonly ILanguageDiscovery _languageDiscovery;
    private readonly CliExecutionContext _executionContext;
    private readonly IEnvironment _environment;
    private readonly ILogger<TypeScriptAppHostToolingCheck> _logger;
    private readonly Func<string, string?> _commandResolver;
 
    public TypeScriptAppHostToolingCheck(
        IProjectLocator projectLocator,
        ILanguageDiscovery languageDiscovery,
        CliExecutionContext executionContext,
        IEnvironment environment,
        ILogger<TypeScriptAppHostToolingCheck> logger)
        : this(projectLocator, languageDiscovery, executionContext, environment, logger, PathLookupHelper.FindFullPathFromPath)
    {
    }
 
    internal TypeScriptAppHostToolingCheck(
        IProjectLocator projectLocator,
        ILanguageDiscovery languageDiscovery,
        CliExecutionContext executionContext,
        IEnvironment environment,
        ILogger<TypeScriptAppHostToolingCheck> logger,
        Func<string, string?> commandResolver)
    {
        _projectLocator = projectLocator;
        _languageDiscovery = languageDiscovery;
        _executionContext = executionContext;
        _environment = environment;
        _logger = logger;
        _commandResolver = commandResolver;
    }
 
    public int Order => 31;
 
    public async Task<IReadOnlyList<EnvironmentCheckResult>> CheckAsync(CancellationToken cancellationToken = default)
    {
        var appHostFile = await ResolveTypeScriptAppHostAsync(cancellationToken);
        if (appHostFile?.Directory is not { Exists: true } appHostDirectory)
        {
            return [];
        }
 
        TypeScriptAppHostToolchain toolchain;
        try
        {
            toolchain = TypeScriptAppHostToolchainResolver.Resolve(appHostDirectory, _environment, _logger);
        }
        catch (YarnClassicNotSupportedException ex)
        {
            return
            [
                new EnvironmentCheckResult
                {
                    Category = EnvironmentCheckCategories.Environment,
                    Name = YarnClassicCheckName,
                    Status = EnvironmentCheckStatus.Fail,
                    Message = "TypeScript AppHost does not support Yarn Classic.",
                    Details = ex.Message,
                    Fix = "Upgrade to Yarn 4 or later, or switch to npm, pnpm, or Bun, then rerun 'aspire doctor'.",
                    Link = "https://yarnpkg.com/getting-started/install",
                    Metadata = new JsonObject
                    {
                        ["language"] = KnownLanguageId.TypeScript,
                        ["appHostPath"] = appHostFile.FullName
                    }
                }
            ];
        }
 
        var missingResults = new List<EnvironmentCheckResult>();
 
        foreach (var command in TypeScriptAppHostToolchainResolver.GetRequiredCommands(toolchain))
        {
            if (CommandPathResolver.TryResolveCommand(command, _commandResolver, out _, out var errorMessage))
            {
                continue;
            }
 
            missingResults.Add(new EnvironmentCheckResult
            {
                Category = EnvironmentCheckCategories.Environment,
                Name = GetMissingCommandCheckName(command),
                Status = EnvironmentCheckStatus.Fail,
                Message = $"TypeScript AppHost requires '{command}'.",
                Details = errorMessage,
                Fix = $"Install {TypeScriptAppHostToolchainResolver.GetDisplayName(toolchain)} tooling and rerun 'aspire doctor'.",
                Link = CommandPathResolver.GetInstallationLink(command),
                Metadata = new JsonObject
                {
                    ["language"] = KnownLanguageId.TypeScript,
                    ["toolchain"] = TypeScriptAppHostToolchainResolver.GetCommandName(toolchain),
                    ["command"] = command
                }
            });
        }
 
        if (missingResults.Count > 0)
        {
            return missingResults;
        }
 
        return
        [
            new EnvironmentCheckResult
            {
                Category = EnvironmentCheckCategories.Environment,
                Name = ToolsCheckName,
                Status = EnvironmentCheckStatus.Pass,
                Message = $"TypeScript AppHost tooling found ({string.Join(", ", TypeScriptAppHostToolchainResolver.GetRequiredCommands(toolchain))}).",
                Metadata = new JsonObject
                {
                    ["language"] = KnownLanguageId.TypeScript,
                    ["toolchain"] = TypeScriptAppHostToolchainResolver.GetCommandName(toolchain),
                    ["appHostPath"] = appHostFile.FullName
                }
            }
        ];
    }
 
    // Delegates to the shared resolver so the doctor tooling check and `aspire update --migrate` stay in
    // lockstep on how the TypeScript AppHost entry point is located.
    private Task<FileInfo?> ResolveTypeScriptAppHostAsync(CancellationToken cancellationToken)
        => LegacyTypeScriptAppHost.ResolveTypeScriptAppHostAsync(
            _projectLocator,
            _languageDiscovery,
            _executionContext.WorkingDirectory,
            _logger,
            cancellationToken);
 
    internal static string GetMissingCommandCheckName(string command) => $"typescript-apphost-{command}";
}