File: AppHostServerV2CodeGeneration.cs
Web Access
Project: src\src\Aspire.Managed\Aspire.Managed.csproj (aspire-managed)
// 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;
using Aspire.Hosting.CodeGeneration.TypeScript;
using Aspire.TypeSystem;
 
namespace Aspire.Managed;
 
/// <summary>Merges loaded provider contracts and emits SDK files using the ATS generator.</summary>
internal static class AppHostServerV2CodeGeneration
{
    internal static async Task<int> GenerateAsync(string requestPath)
    {
        var input = await AppHostServerV2CodeGenerationInput.ReadAsync(requestPath).ConfigureAwait(false);
        var files = Generate(input);
        Directory.CreateDirectory(input.Output);
        foreach (var (name, content) in files)
        {
            await File.WriteAllTextAsync(Path.Combine(input.Output, name), content).ConfigureAwait(false);
        }
 
        return 0;
    }
 
    internal static IReadOnlyDictionary<string, string> Generate(AppHostServerV2CodeGenerationInput input)
    {
        var core = input.Core;
        var capabilities = core.Capabilities.ToList();
        var handles = core.HandleTypes.ToDictionary(type => type.AtsTypeId, StringComparer.Ordinal);
        var ids = capabilities.Select(capability => capability.CapabilityId).ToHashSet(StringComparer.Ordinal);
        var sdkFiles = new Dictionary<string, string>(StringComparer.Ordinal);
        foreach (var provider in input.Providers)
        {
            foreach (var export in provider.Capabilities)
            {
                if (export.LifecyclePhase is not null)
                {
                    continue;
                }
                var capability = export.Capability;
                if (!capability.CapabilityId.StartsWith(provider.PackageName + "/", StringComparison.Ordinal) ||
                    !ids.Add(capability.CapabilityId))
                {
                    throw new InvalidDataException($"Provider capability '{capability.CapabilityId}' is duplicate or outside its package.");
                }
                capabilities.Add(capability);
                foreach (var type in capability.Parameters.Select(parameter => parameter.Type)
                    .Append(capability.ReturnType).Where(type => type is not null))
                {
                    AddHandle(type!);
                }
                foreach (var parameter in capability.Parameters.Where(parameter => parameter.IsCallback))
                {
                    foreach (var callbackParameter in parameter.CallbackParameters ?? [])
                    {
                        AddHandle(callbackParameter.Type);
                    }
                    if (parameter.CallbackReturnType is { } callbackReturn)
                    {
                        AddHandle(callbackReturn);
                    }
                }
            }
            foreach (var (name, content) in provider.SdkFiles)
            {
                if (!sdkFiles.TryAdd(name, content))
                {
                    throw new InvalidDataException($"Provider SDK file '{name}' is duplicated.");
                }
            }
        }
        var context = new AtsContext
        {
            Capabilities = capabilities,
            HandleTypes = handles.Values.ToArray(),
            DtoTypes = core.DtoTypes,
            EnumTypes = core.EnumTypes,
            ExportedValues = core.ExportedValues
        };
        var files = new AtsTypeScriptCodeGenerator().GenerateDistributedApplication(context, includeManagedBootstrap: false);
        var result = new Dictionary<string, string>(StringComparer.Ordinal);
        foreach (var (name, content) in files)
        {
            if (sdkFiles.ContainsKey(name))
            {
                throw new InvalidDataException($"Provider SDK file '{name}' replaces a core SDK file.");
            }
            var projected = content;
            if (name == "aspire.mts")
            {
                projected += "\nexport { createBuilder, DistributedApplicationBuilder, ResourceBuilder, Resource, ContainerResource, ExecutableResource, ParameterResource, ComputeEnvironmentResource, ReferenceExpression, EndpointReference, ExecutableCommand, NativeBuildContext, NativeApplication } from './native-builder.mjs';\n";
                projected += "export type { ParameterOptions, ParameterGenerationOptions, PublishExpression, PublishResource, PublishModel, PublishingContext, DeploymentTarget } from './native-builder.mjs';\n";
                projected += "export { NativePipeline, WellKnownPipelineSteps, WellKnownPipelineTags } from './native-pipeline.mjs';\nexport type { NativePipelineStepContext, NativePipelineConfigurationContext, NativePipelineStepOptions, NativePipelineAction } from './native-pipeline.mjs';\n";
                foreach (var provider in input.Providers)
                {
                    foreach (var module in provider.Exports)
                    {
                        projected += "export * from " + JsonSerializer.Serialize("./" + module[..^4] + ".mjs") + ";\n";
                    }
                }
            }
            result.Add(name, projected);
        }
        foreach (var (name, content) in sdkFiles)
        {
            result.Add(name, content);
        }
 
        return result;
 
        void AddHandle(AtsTypeRef type)
        {
            if (type.Category == AtsTypeCategory.Handle)
            {
                handles.TryAdd(type.TypeId, new AtsTypeInfo { AtsTypeId = type.TypeId });
            }
            if (type.ElementType is { } element)
            {
                AddHandle(element);
            }
        }
    }
}