diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a487612b..0537ff002 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,7 +64,7 @@ jobs: shell: bash run: | install_path="$RUNNER_TEMP/aspire-cli" - curl -sSL https://aspire.dev/install.sh | bash -s -- --install-path "$install_path" --skip-path + curl -sSL https://aspire.dev/install.sh | bash -s -- --install-path "$install_path" --skip-path --quality staging echo "$install_path" >> "$GITHUB_PATH" echo "ASPIRE_CLI=$install_path/aspire" >> "$GITHUB_ENV" "$install_path/aspire" --version @@ -78,7 +78,7 @@ jobs: $aspirePath = Join-Path $installPath 'aspire.exe' Invoke-RestMethod 'https://aspire.dev/install.ps1' -OutFile $installScript - & $installScript -InstallPath $installPath -SkipPath + & $installScript -InstallPath $installPath -SkipPath -Quality staging $installPath | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append "ASPIRE_CLI=$aspirePath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append diff --git a/README.md b/README.md index c9b1edd2d..7e48e399e 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Samples for [Aspire](https://aspire.dev). | Sample | Workload languages | AppHost | Deploy | Description | | --- | --- | --- | --- | --- | | [Integrating a Go App](./samples/container-build) | Go | C# AppHost | Docker Compose | Builds and runs a Go Gin app from a Dockerfile with Aspire. | +| [Custom MailDev and MailKit integrations](./samples/maildev-mailkit) | C#, TypeScript | TypeScript AppHost | Run only | Authors custom hosting and client integrations with ATS-generated TypeScript APIs. | | [Go API](./samples/golang-api) | Go | TypeScript AppHost | Docker Compose | Go + chi API with Aspire-managed run and publish flows. | | [Python FastAPI + PostgreSQL](./samples/python-fastapi-postgres) | Python | TypeScript AppHost | Docker Compose | FastAPI CRUD API wired to PostgreSQL and pgAdmin. | | [Python OpenAI Agent](./samples/python-openai-agent) | Python | TypeScript AppHost | Docker Compose | FastAPI AI agent sample with OpenAI integration. | diff --git a/samples/maildev-mailkit/.gitignore b/samples/maildev-mailkit/.gitignore new file mode 100644 index 000000000..da67b2f63 --- /dev/null +++ b/samples/maildev-mailkit/.gitignore @@ -0,0 +1,3 @@ +.aspire/ +**/bin/ +**/obj/ \ No newline at end of file diff --git a/samples/maildev-mailkit/CSharpAppHost/CSharpAppHost.csproj b/samples/maildev-mailkit/CSharpAppHost/CSharpAppHost.csproj new file mode 100644 index 000000000..fba2d2d75 --- /dev/null +++ b/samples/maildev-mailkit/CSharpAppHost/CSharpAppHost.csproj @@ -0,0 +1,18 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + \ No newline at end of file diff --git a/samples/maildev-mailkit/CSharpAppHost/Program.cs b/samples/maildev-mailkit/CSharpAppHost/Program.cs new file mode 100644 index 000000000..5b7731590 --- /dev/null +++ b/samples/maildev-mailkit/CSharpAppHost/Program.cs @@ -0,0 +1,13 @@ +using Aspire.Hosting; + +var builder = DistributedApplication.CreateBuilder(args); + +var maildev = builder.AddMailDev("maildev"); + +builder.AddProject("newsletterservice", "../NewsletterService/NewsletterService.csproj") + .WithHttpHealthCheck("/health") + .WithExternalHttpEndpoints() + .WithReference(maildev) + .WaitFor(maildev); + +builder.Build().Run(); \ No newline at end of file diff --git a/samples/maildev-mailkit/MailDev.Hosting.Tests/MailDev.Hosting.Tests.csproj b/samples/maildev-mailkit/MailDev.Hosting.Tests/MailDev.Hosting.Tests.csproj new file mode 100644 index 000000000..e63689c3c --- /dev/null +++ b/samples/maildev-mailkit/MailDev.Hosting.Tests/MailDev.Hosting.Tests.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/maildev-mailkit/MailDev.Hosting.Tests/MailDevResourceTests.cs b/samples/maildev-mailkit/MailDev.Hosting.Tests/MailDevResourceTests.cs new file mode 100644 index 000000000..02a0670e9 --- /dev/null +++ b/samples/maildev-mailkit/MailDev.Hosting.Tests/MailDevResourceTests.cs @@ -0,0 +1,84 @@ +using Aspire.Hosting; +using Aspire.Hosting.ApplicationModel; +using System.Reflection; +using Xunit; + +namespace MailDev.Hosting.Tests; + +public sealed class MailDevResourceTests +{ + [Fact] + public void MailDevHostingSurfaceIsExportedForAts() + { + var method = typeof(MailDevResourceBuilderExtensions).GetMethod( + nameof(MailDevResourceBuilderExtensions.AddMailDev), + BindingFlags.Public | BindingFlags.Static); + + Assert.NotNull(method); + Assert.Contains( + method.GetCustomAttributes(), + attribute => attribute.GetType().Name == "AspireExportAttribute"); + Assert.Contains( + typeof(MailDevResource).GetCustomAttributes(), + attribute => attribute.GetType().Name == "AspireExportAttribute"); + + var nameParameter = Assert.Single( + method.GetParameters(), + parameter => parameter.Name == "name"); + Assert.Contains( + nameParameter.GetCustomAttributes(), + attribute => attribute.GetType().Name == "ResourceNameAttribute"); + } + + [Fact] + public void AddMailDevConfiguresContainerAndEndpoints() + { + var builder = DistributedApplication.CreateBuilder(); + + var maildev = builder.AddMailDev("maildev"); + + var image = Assert.Single(maildev.Resource.Annotations.OfType()); + Assert.Equal("docker.io", image.Registry); + Assert.Equal("maildev/maildev", image.Image); + Assert.Equal("2.2.1", image.Tag); + + var endpoints = maildev.Resource.Annotations.OfType().ToArray(); + var http = Assert.Single(endpoints, endpoint => endpoint.Name == "http"); + var smtp = Assert.Single(endpoints, endpoint => endpoint.Name == "smtp"); + Assert.Equal(1080, http.TargetPort); + Assert.Equal(1025, smtp.TargetPort); + } + + [Fact] + public void AddMailDevCreatesSecretPasswordAndDeferredConnectionString() + { + var builder = DistributedApplication.CreateBuilder(); + + var maildev = builder.AddMailDev("maildev"); + + Assert.True(maildev.Resource.PasswordParameter.Secret); + Assert.Null(maildev.Resource.UsernameParameter); + Assert.Equal( + "Endpoint=smtp://{maildev.bindings.smtp.host}:{maildev.bindings.smtp.port};Username=mail-dev;Password={maildev-password.value}", + maildev.Resource.ConnectionStringExpression.ValueExpression); + } + + [Fact] + public void AddMailDevUsesProvidedCredentialParameters() + { + var builder = DistributedApplication.CreateBuilder(); + var username = builder.AddParameter("smtp-user"); + var password = builder.AddParameter("smtp-password", secret: true); + + var maildev = builder.AddMailDev( + "maildev", + username: username, + password: password); + + Assert.Same(username.Resource, maildev.Resource.UsernameParameter); + Assert.Same(password.Resource, maildev.Resource.PasswordParameter); + Assert.Equal( + "Endpoint=smtp://{maildev.bindings.smtp.host}:{maildev.bindings.smtp.port};Username={smtp-user.value};Password={smtp-password.value}", + maildev.Resource.ConnectionStringExpression.ValueExpression); + } +} \ No newline at end of file diff --git a/samples/maildev-mailkit/MailDev.Hosting/MailDev.Hosting.csproj b/samples/maildev-mailkit/MailDev.Hosting/MailDev.Hosting.csproj new file mode 100644 index 000000000..cf75a679f --- /dev/null +++ b/samples/maildev-mailkit/MailDev.Hosting/MailDev.Hosting.csproj @@ -0,0 +1,14 @@ + + + + net10.0 + enable + enable + true + + + + + + + \ No newline at end of file diff --git a/samples/maildev-mailkit/MailDev.Hosting/MailDevResource.cs b/samples/maildev-mailkit/MailDev.Hosting/MailDevResource.cs new file mode 100644 index 000000000..246e6bc4b --- /dev/null +++ b/samples/maildev-mailkit/MailDev.Hosting/MailDevResource.cs @@ -0,0 +1,36 @@ +namespace Aspire.Hosting.ApplicationModel; + +/// Represents a MailDev container resource. +[AspireExport] +public sealed class MailDevResource( + [ResourceName] string name, + ParameterResource? username, + ParameterResource password) + : ContainerResource(name), IResourceWithConnectionString +{ + internal const string HttpEndpointName = "http"; + internal const string SmtpEndpointName = "smtp"; + + private const string DefaultUsername = "mail-dev"; + private EndpointReference? _smtpEndpoint; + + /// Gets the optional MailDev SMTP username parameter. + public ParameterResource? UsernameParameter { get; } = username; + + /// Gets the MailDev SMTP password parameter. + public ParameterResource PasswordParameter { get; } = password; + + internal ReferenceExpression UsernameReference => + UsernameParameter is not null + ? ReferenceExpression.Create($"{UsernameParameter}") + : ReferenceExpression.Create($"{DefaultUsername}"); + + /// Gets the MailDev SMTP endpoint. + public EndpointReference SmtpEndpoint => + _smtpEndpoint ??= new(this, SmtpEndpointName); + + /// + public ReferenceExpression ConnectionStringExpression => + ReferenceExpression.Create( + $"Endpoint=smtp://{SmtpEndpoint.Property(EndpointProperty.HostAndPort)};Username={UsernameReference};Password={PasswordParameter}"); +} \ No newline at end of file diff --git a/samples/maildev-mailkit/MailDev.Hosting/MailDevResourceBuilderExtensions.cs b/samples/maildev-mailkit/MailDev.Hosting/MailDevResourceBuilderExtensions.cs new file mode 100644 index 000000000..3b934df54 --- /dev/null +++ b/samples/maildev-mailkit/MailDev.Hosting/MailDevResourceBuilderExtensions.cs @@ -0,0 +1,56 @@ +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting; + +/// Provides extension methods for adding MailDev resources. +public static class MailDevResourceBuilderExtensions +{ + private const string Registry = "docker.io"; + private const string Image = "maildev/maildev"; + private const string Tag = "2.2.1"; + private const string UsernameEnvironmentVariable = "MAILDEV_INCOMING_USER"; + private const string PasswordEnvironmentVariable = "MAILDEV_INCOMING_PASS"; + + /// Adds a MailDev container resource. + /// The distributed application builder. + /// The resource name. + /// The optional host port for the MailDev web interface. + /// The optional host port for SMTP. + /// The optional SMTP username parameter. + /// The optional SMTP password parameter. + /// The MailDev resource builder. + [AspireExport] + public static IResourceBuilder AddMailDev( + this IDistributedApplicationBuilder builder, + [ResourceName] string name, + int? httpPort = null, + int? smtpPort = null, + IResourceBuilder? username = null, + IResourceBuilder? password = null) + { + ArgumentNullException.ThrowIfNull(builder); + + var passwordParameter = password?.Resource ?? + ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter( + builder, $"{name}-password"); + var resource = new MailDevResource(name, username?.Resource, passwordParameter); + + return builder.AddResource(resource) + .WithImage(Image) + .WithImageRegistry(Registry) + .WithImageTag(Tag) + .WithHttpEndpoint( + targetPort: 1080, + port: httpPort, + name: MailDevResource.HttpEndpointName) + .WithEndpoint( + targetPort: 1025, + port: smtpPort, + name: MailDevResource.SmtpEndpointName) + .WithEnvironment(context => + { + context.EnvironmentVariables[UsernameEnvironmentVariable] = resource.UsernameReference; + context.EnvironmentVariables[PasswordEnvironmentVariable] = resource.PasswordParameter; + }); + } +} \ No newline at end of file diff --git a/samples/maildev-mailkit/MailKit.Client.Tests/MailKit.Client.Tests.csproj b/samples/maildev-mailkit/MailKit.Client.Tests/MailKit.Client.Tests.csproj new file mode 100644 index 000000000..bac29a321 --- /dev/null +++ b/samples/maildev-mailkit/MailKit.Client.Tests/MailKit.Client.Tests.csproj @@ -0,0 +1,21 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/maildev-mailkit/MailKit.Client.Tests/MailKitClientSettingsTests.cs b/samples/maildev-mailkit/MailKit.Client.Tests/MailKitClientSettingsTests.cs new file mode 100644 index 000000000..ff3f08521 --- /dev/null +++ b/samples/maildev-mailkit/MailKit.Client.Tests/MailKitClientSettingsTests.cs @@ -0,0 +1,43 @@ +using MailKit.Client; +using Xunit; + +namespace MailKit.Client.Tests; + +public sealed class MailKitClientSettingsTests +{ + [Fact] + public void ParseConnectionStringAcceptsUri() + { + var settings = new MailKitClientSettings(); + + settings.ParseConnectionString("smtp://localhost:1025"); + + Assert.Equal(new Uri("smtp://localhost:1025"), settings.Endpoint); + Assert.Null(settings.Credentials); + } + + [Fact] + public void ParseConnectionStringAcceptsEndpointAndCredentials() + { + var settings = new MailKitClientSettings(); + + settings.ParseConnectionString( + "Endpoint=smtp://localhost:1025;Username=mail-dev;Password=secret"); + + Assert.Equal(new Uri("smtp://localhost:1025"), settings.Endpoint); + Assert.Equal("mail-dev", settings.Credentials?.UserName); + Assert.Equal("secret", settings.Credentials?.Password); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("Host=localhost")] + public void ParseConnectionStringRejectsMissingEndpoint(string? connectionString) + { + var settings = new MailKitClientSettings(); + + Assert.Throws( + () => settings.ParseConnectionString(connectionString)); + } +} \ No newline at end of file diff --git a/samples/maildev-mailkit/MailKit.Client.Tests/MailKitExtensionsTests.cs b/samples/maildev-mailkit/MailKit.Client.Tests/MailKitExtensionsTests.cs new file mode 100644 index 000000000..b1160fb32 --- /dev/null +++ b/samples/maildev-mailkit/MailKit.Client.Tests/MailKitExtensionsTests.cs @@ -0,0 +1,79 @@ +using MailKit.Client; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Xunit; + +namespace MailKit.Client.Tests; + +public sealed class MailKitExtensionsTests +{ + [Fact] + public void AddMailKitClientRegistersOneFactoryPerScope() + { + var builder = Host.CreateApplicationBuilder(); + builder.Configuration["ConnectionStrings:maildev"] = "smtp://localhost:1025"; + builder.AddMailKitClient("maildev"); + using var provider = builder.Services.BuildServiceProvider(); + + using var firstScope = provider.CreateScope(); + using var secondScope = provider.CreateScope(); + + var first = firstScope.ServiceProvider.GetRequiredService(); + Assert.Same(first, firstScope.ServiceProvider.GetRequiredService()); + Assert.NotSame(first, secondScope.ServiceProvider.GetRequiredService()); + Assert.NotNull(provider.GetRequiredService()); + } + + [Fact] + public void AddKeyedMailKitClientRegistersNamedFactory() + { + var builder = Host.CreateApplicationBuilder(); + builder.Configuration["ConnectionStrings:transactional"] = "smtp://localhost:1025"; + builder.AddKeyedMailKitClient("transactional"); + using var provider = builder.Services.BuildServiceProvider(); + using var scope = provider.CreateScope(); + + Assert.NotNull( + scope.ServiceProvider.GetRequiredKeyedService("transactional")); + Assert.Null(scope.ServiceProvider.GetService()); + } + + [Fact] + public void AddMailKitClientUsesConnectionNameForHealthCheck() + { + var builder = Host.CreateApplicationBuilder(); + builder.Configuration["ConnectionStrings:primary"] = "smtp://localhost:1025"; + builder.Configuration["ConnectionStrings:secondary"] = "smtp://localhost:1026"; + + builder.AddMailKitClient("primary"); + builder.AddMailKitClient("secondary"); + + using var provider = builder.Services.BuildServiceProvider(); + var options = provider.GetRequiredService>().Value; + + Assert.Collection( + options.Registrations, + registration => Assert.Equal("MailKit_primary", registration.Name), + registration => Assert.Equal("MailKit_secondary", registration.Name)); + } + + [Fact] + public void FactoryValidationIsDeferredUntilResolution() + { + var builder = Host.CreateApplicationBuilder(); + + builder.AddMailKitClient("maildev", settings => + { + settings.DisableHealthChecks = true; + settings.DisableTracing = true; + settings.DisableMetrics = true; + }); + + using var provider = builder.Services.BuildServiceProvider(); + using var scope = provider.CreateScope(); + Assert.Throws( + scope.ServiceProvider.GetRequiredService); + } +} \ No newline at end of file diff --git a/samples/maildev-mailkit/MailKit.Client/MailKit.Client.csproj b/samples/maildev-mailkit/MailKit.Client/MailKit.Client.csproj new file mode 100644 index 000000000..3d27f28e3 --- /dev/null +++ b/samples/maildev-mailkit/MailKit.Client/MailKit.Client.csproj @@ -0,0 +1,17 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + \ No newline at end of file diff --git a/samples/maildev-mailkit/MailKit.Client/MailKitClientFactory.cs b/samples/maildev-mailkit/MailKit.Client/MailKitClientFactory.cs new file mode 100644 index 000000000..2df0b1936 --- /dev/null +++ b/samples/maildev-mailkit/MailKit.Client/MailKitClientFactory.cs @@ -0,0 +1,57 @@ +using MailKit.Net.Smtp; + +namespace MailKit.Client; + +/// Creates and reuses a connected SMTP client for the current scope. +public sealed class MailKitClientFactory(MailKitClientSettings settings) : IDisposable +{ + private readonly SemaphoreSlim _semaphore = new(1, 1); + private SmtpClient? _client; + + /// Gets a connected and, when configured, authenticated SMTP client. + public async Task GetSmtpClientAsync( + CancellationToken cancellationToken = default) + { + await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + if (_client is not null) + { + return _client; + } + + var endpoint = settings.Endpoint ?? + throw new InvalidOperationException("The MailKit SMTP endpoint is not configured."); + var client = new SmtpClient(); + + try + { + await client.ConnectAsync(endpoint, cancellationToken).ConfigureAwait(false); + if (settings.Credentials is not null) + { + await client.AuthenticateAsync(settings.Credentials, cancellationToken) + .ConfigureAwait(false); + } + + _client = client; + return client; + } + catch + { + client.Dispose(); + throw; + } + } + finally + { + _semaphore.Release(); + } + } + + /// + public void Dispose() + { + _client?.Dispose(); + _semaphore.Dispose(); + } +} \ No newline at end of file diff --git a/samples/maildev-mailkit/MailKit.Client/MailKitClientSettings.cs b/samples/maildev-mailkit/MailKit.Client/MailKitClientSettings.cs new file mode 100644 index 000000000..bc4923e77 --- /dev/null +++ b/samples/maildev-mailkit/MailKit.Client/MailKitClientSettings.cs @@ -0,0 +1,54 @@ +using System.Data.Common; +using System.Net; + +namespace MailKit.Client; + +/// Provides settings for connecting a MailKit SMTP client. +public sealed class MailKitClientSettings +{ + internal const string DefaultConfigSectionName = "MailKit:Client"; + + /// Gets or sets the SMTP endpoint. + public Uri? Endpoint { get; set; } + + /// Gets or sets optional SMTP credentials. + public NetworkCredential? Credentials { get; set; } + + /// Gets or sets whether health checks are disabled. + public bool DisableHealthChecks { get; set; } + + /// Gets or sets whether tracing is disabled. + public bool DisableTracing { get; set; } + + /// Gets or sets whether metrics are disabled. + public bool DisableMetrics { get; set; } + + internal void ParseConnectionString(string? connectionString) + { + if (string.IsNullOrWhiteSpace(connectionString)) + { + throw new InvalidOperationException( + $"A connection string must be provided through ConnectionStrings: or {DefaultConfigSectionName}:Endpoint."); + } + + if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri)) + { + Endpoint = uri; + return; + } + + var values = new DbConnectionStringBuilder { ConnectionString = connectionString }; + if (!values.TryGetValue("Endpoint", out var endpoint) || + !Uri.TryCreate(endpoint?.ToString(), UriKind.Absolute, out uri)) + { + throw new InvalidOperationException("The SMTP connection string must contain a valid Endpoint value."); + } + + Endpoint = uri; + if (values.TryGetValue("Username", out var username) && + values.TryGetValue("Password", out var password)) + { + Credentials = new NetworkCredential(username?.ToString(), password?.ToString()); + } + } +} \ No newline at end of file diff --git a/samples/maildev-mailkit/MailKit.Client/MailKitExtensions.cs b/samples/maildev-mailkit/MailKit.Client/MailKitExtensions.cs new file mode 100644 index 000000000..0a8068f3e --- /dev/null +++ b/samples/maildev-mailkit/MailKit.Client/MailKitExtensions.cs @@ -0,0 +1,106 @@ +using MailKit.Client; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +/// Provides MailKit client registration extensions. +public static class MailKitExtensions +{ + /// Registers a scoped MailKit SMTP client factory. + public static void AddMailKitClient( + this IHostApplicationBuilder builder, + string connectionName, + Action? configureSettings = null) => + AddMailKitClient( + builder, + MailKitClientSettings.DefaultConfigSectionName, + connectionName, + serviceKey: null, + configureSettings); + + /// Registers a keyed scoped MailKit SMTP client factory. + public static void AddKeyedMailKitClient( + this IHostApplicationBuilder builder, + string name, + Action? configureSettings = null) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + AddMailKitClient( + builder, + $"{MailKitClientSettings.DefaultConfigSectionName}:{name}", + name, + name, + configureSettings); + } + + private static void AddMailKitClient( + IHostApplicationBuilder builder, + string configurationSectionName, + string connectionName, + object? serviceKey, + Action? configureSettings) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrWhiteSpace(connectionName); + + var settings = new MailKitClientSettings(); + builder.Configuration.GetSection(configurationSectionName).Bind(settings); + + if (builder.Configuration.GetConnectionString(connectionName) is { } connectionString) + { + settings.ParseConnectionString(connectionString); + } + + configureSettings?.Invoke(settings); + + if (serviceKey is null) + { + builder.Services.AddScoped(_ => CreateFactory(settings)); + } + else + { + builder.Services.AddKeyedScoped( + serviceKey, + (_, _) => CreateFactory(settings)); + } + + if (!settings.DisableHealthChecks) + { + builder.Services.AddHealthChecks().Add(new HealthCheckRegistration( + $"MailKit_{connectionName}", + serviceProvider => new MailKitHealthCheck( + serviceKey is null + ? serviceProvider.GetRequiredService() + : serviceProvider.GetRequiredKeyedService(serviceKey)), + failureStatus: null, + tags: [])); + } + + if (!settings.DisableTracing) + { + builder.Services.AddOpenTelemetry() + .WithTracing(tracing => tracing.AddSource(MailKit.Telemetry.SmtpClient.ActivitySourceName)); + } + + if (!settings.DisableMetrics) + { + MailKit.Telemetry.SmtpClient.Configure(); + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => metrics.AddMeter(MailKit.Telemetry.SmtpClient.MeterName)); + } + } + + private static MailKitClientFactory CreateFactory(MailKitClientSettings settings) + { + if (settings.Endpoint is null) + { + throw new InvalidOperationException("The MailKit SMTP endpoint is not configured."); + } + + return new MailKitClientFactory(settings); + } +} \ No newline at end of file diff --git a/samples/maildev-mailkit/MailKit.Client/MailKitHealthCheck.cs b/samples/maildev-mailkit/MailKit.Client/MailKitHealthCheck.cs new file mode 100644 index 000000000..bfc2327d2 --- /dev/null +++ b/samples/maildev-mailkit/MailKit.Client/MailKitHealthCheck.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace MailKit.Client; + +internal sealed class MailKitHealthCheck(MailKitClientFactory factory) : IHealthCheck +{ + public async Task CheckHealthAsync( + HealthCheckContext context, + CancellationToken cancellationToken = default) + { + try + { + _ = await factory.GetSmtpClientAsync(cancellationToken).ConfigureAwait(false); + return HealthCheckResult.Healthy(); + } + catch (Exception exception) + { + return HealthCheckResult.Unhealthy(exception: exception); + } + } +} \ No newline at end of file diff --git a/samples/maildev-mailkit/MailKit.Client/Properties/AssemblyInfo.cs b/samples/maildev-mailkit/MailKit.Client/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..11e4e3f8b --- /dev/null +++ b/samples/maildev-mailkit/MailKit.Client/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("MailKit.Client.Tests")] \ No newline at end of file diff --git a/samples/maildev-mailkit/NewsletterService/NewsletterService.csproj b/samples/maildev-mailkit/NewsletterService/NewsletterService.csproj new file mode 100644 index 000000000..58879a439 --- /dev/null +++ b/samples/maildev-mailkit/NewsletterService/NewsletterService.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/maildev-mailkit/NewsletterService/Program.cs b/samples/maildev-mailkit/NewsletterService/Program.cs new file mode 100644 index 000000000..dcf93e109 --- /dev/null +++ b/samples/maildev-mailkit/NewsletterService/Program.cs @@ -0,0 +1,61 @@ +using MailKit.Client; +using MimeKit; +using Scalar.AspNetCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); +builder.AddMailKitClient("maildev"); +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +app.MapOpenApi(); +app.MapScalarApiReference(); +app.MapDefaultEndpoints(); + +app.MapPost("/subscribe", async ( + SubscriptionRequest request, + MailKitClientFactory mailKit, + CancellationToken cancellationToken) => +{ + var message = CreateMessage( + request.Email, + "Welcome to the Aspire newsletter", + "You are now subscribed to the Aspire newsletter."); + var client = await mailKit.GetSmtpClientAsync(cancellationToken); + await client.SendAsync(message, cancellationToken); + + return Results.Accepted(value: new { request.Email, Status = "subscribed" }); +}) +.WithName("Subscribe"); + +app.MapPost("/unsubscribe", async ( + SubscriptionRequest request, + MailKitClientFactory mailKit, + CancellationToken cancellationToken) => +{ + var message = CreateMessage( + request.Email, + "Aspire newsletter subscription ended", + "You have been unsubscribed from the Aspire newsletter."); + var client = await mailKit.GetSmtpClientAsync(cancellationToken); + await client.SendAsync(message, cancellationToken); + + return Results.Ok(new { request.Email, Status = "unsubscribed" }); +}) +.WithName("Unsubscribe"); + +app.Run(); + +static MimeMessage CreateMessage(string email, string subject, string body) +{ + var message = new MimeMessage(); + message.From.Add(new MailboxAddress("Aspire Newsletter", "newsletter@example.com")); + message.To.Add(MailboxAddress.Parse(email)); + message.Subject = subject; + message.Body = new TextPart("plain") { Text = body }; + return message; +} + +internal sealed record SubscriptionRequest(string Email); \ No newline at end of file diff --git a/samples/maildev-mailkit/NewsletterService/Properties/launchSettings.json b/samples/maildev-mailkit/NewsletterService/Properties/launchSettings.json new file mode 100644 index 000000000..a7c26e31f --- /dev/null +++ b/samples/maildev-mailkit/NewsletterService/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5274", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7274;http://localhost:5274", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/samples/maildev-mailkit/NuGet.Config b/samples/maildev-mailkit/NuGet.Config new file mode 100644 index 000000000..149e76950 --- /dev/null +++ b/samples/maildev-mailkit/NuGet.Config @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/samples/maildev-mailkit/README.md b/samples/maildev-mailkit/README.md new file mode 100644 index 000000000..3efc90aef --- /dev/null +++ b/samples/maildev-mailkit/README.md @@ -0,0 +1,90 @@ +# MailDev and MailKit custom integrations + +This sample demonstrates how to build custom .NET Aspire hosting and client integrations with Aspire 13.5. The runnable AppHost is written in TypeScript and consumes the C# hosting integration through the Aspire Type System (ATS). + +## Projects + +- `MailDev.Hosting` models a MailDev container, its web and SMTP endpoints, and a deferred connection string. +- `MailKit.Client` registers a scoped MailKit SMTP client, health checks, tracing, and metrics in a consuming service. +- `NewsletterService` sends subscription and unsubscription messages through the integrations. +- `ServiceDefaults` configures standard Aspire health checks and OpenTelemetry. +- `CSharpAppHost` is a compile-validated C# equivalent of the runnable TypeScript AppHost. + +## Aspire Type System + +The hosting integration marks `MailDevResource` and `AddMailDev` with `[AspireExport]`. The local project reference in `aspire.config.json` lets `aspire restore` inspect those exports and generate the TypeScript `addMailDev` API under `.aspire/modules`. + +Generated files under `.aspire/modules` are not source files and must not be edited or committed. + +```json +"packages": { + "MailDev.Hosting": "MailDev.Hosting/MailDev.Hosting.csproj" +} +``` + +The TypeScript AppHost uses the generated API: + +```typescript +const maildev = await builder.addMailDev("maildev"); + +await builder.addCSharpApp("newsletterservice", "./NewsletterService") + .withHttpHealthCheck({ path: "/health" }) + .withExternalHttpEndpoints() + .withReference(maildev) + .waitFor(maildev); +``` + +The equivalent C# AppHost code is: + +```csharp +var maildev = builder.AddMailDev("maildev"); + +builder.AddProject("newsletterservice", "../NewsletterService/NewsletterService.csproj") + .WithHttpHealthCheck("/health") + .WithExternalHttpEndpoints() + .WithReference(maildev) + .WaitFor(maildev); +``` + +## Secure credentials + +`AddMailDev` creates a secret password parameter by default. The password is passed to MailDev through `MAILDEV_INCOMING_PASS` and to the newsletter service through a deferred connection string: + +```text +Endpoint=smtp://{maildev.bindings.smtp.host}:{maildev.bindings.smtp.port};Username=mail-dev;Password={maildev-password.value} +``` + +The AppHost model retains parameter and endpoint references instead of embedding a password or allocated port in source code. MailKit parses the resolved connection string, connects to SMTP, and authenticates for each service scope. + +## Run the sample + +Prerequisites are .NET 10, Node.js 24 or a supported Node.js 20/22 release, Docker, and Aspire CLI 13.5. + +```powershell +aspire update --self --channel staging +aspire restore +npm install +npm run aspire:build +aspire run +``` + +Use the newsletter service endpoint shown in the Aspire dashboard: + +```http +POST /subscribe +Content-Type: application/json + +{ "email": "reader@example.com" } +``` + +Open the MailDev web endpoint from the dashboard to inspect the generated message. Use `POST /unsubscribe` with the same payload to send the unsubscription message. + +## Tests + +```powershell +dotnet test MailDev.Hosting.Tests/MailDev.Hosting.Tests.csproj +dotnet test MailKit.Client.Tests/MailKit.Client.Tests.csproj +npm run aspire:build +npm run aspire:lint +dotnet build CSharpAppHost/CSharpAppHost.csproj +``` \ No newline at end of file diff --git a/samples/maildev-mailkit/ServiceDefaults/Extensions.cs b/samples/maildev-mailkit/ServiceDefaults/Extensions.cs new file mode 100644 index 000000000..a663419b3 --- /dev/null +++ b/samples/maildev-mailkit/ServiceDefaults/Extensions.cs @@ -0,0 +1,70 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +public static class Extensions +{ + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + + public static TBuilder AddServiceDefaults(this TBuilder builder) + where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => metrics + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation()) + .WithTracing(tracing => tracing + .AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation(options => + options.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) && + !context.Request.Path.StartsWithSegments(AlivenessEndpointPath)) + .AddHttpClientInstrumentation()); + + if (!string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"])) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + builder.Services.AddHealthChecks() + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + builder.Services.AddServiceDiscovery(); + builder.Services.ConfigureHttpClientDefaults(http => + { + http.AddStandardResilienceHandler(); + http.AddServiceDiscovery(); + }); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + if (app.Environment.IsDevelopment()) + { + app.MapHealthChecks(HealthEndpointPath); + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions + { + Predicate = check => check.Tags.Contains("live") + }); + } + + return app; + } +} \ No newline at end of file diff --git a/samples/maildev-mailkit/ServiceDefaults/ServiceDefaults.csproj b/samples/maildev-mailkit/ServiceDefaults/ServiceDefaults.csproj new file mode 100644 index 000000000..f3f1bcf36 --- /dev/null +++ b/samples/maildev-mailkit/ServiceDefaults/ServiceDefaults.csproj @@ -0,0 +1,24 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/maildev-mailkit/apphost.mts b/samples/maildev-mailkit/apphost.mts new file mode 100644 index 000000000..a44a25295 --- /dev/null +++ b/samples/maildev-mailkit/apphost.mts @@ -0,0 +1,13 @@ +import { createBuilder } from "./.aspire/modules/aspire.mjs"; + +const builder = await createBuilder(); + +const maildev = await builder.addMailDev("maildev"); + +await builder.addCSharpApp("newsletterservice", "./NewsletterService") + .withHttpHealthCheck({ path: "/health" }) + .withExternalHttpEndpoints() + .withReference(maildev) + .waitFor(maildev); + +await builder.build().run(); \ No newline at end of file diff --git a/samples/maildev-mailkit/aspire.config.json b/samples/maildev-mailkit/aspire.config.json new file mode 100644 index 000000000..415ba6d49 --- /dev/null +++ b/samples/maildev-mailkit/aspire.config.json @@ -0,0 +1,13 @@ +{ + "appHost": { + "path": "apphost.mts", + "language": "typescript/nodejs" + }, + "sdk": { + "version": "13.5.0" + }, + "channel": "staging", + "packages": { + "MailDev.Hosting": "MailDev.Hosting/MailDev.Hosting.csproj" + } +} \ No newline at end of file diff --git a/samples/maildev-mailkit/eslint.config.mjs b/samples/maildev-mailkit/eslint.config.mjs new file mode 100644 index 000000000..60b180dcf --- /dev/null +++ b/samples/maildev-mailkit/eslint.config.mjs @@ -0,0 +1,16 @@ +import { defineConfig } from "eslint/config"; +import tseslint from "typescript-eslint"; + +export default defineConfig({ + files: ["apphost.mts"], + extends: [tseslint.configs.base], + languageOptions: { + parserOptions: { + project: "./tsconfig.apphost.json", + tsconfigRootDir: import.meta.dirname + } + }, + rules: { + "@typescript-eslint/no-floating-promises": ["error", { checkThenables: true }] + } +}); \ No newline at end of file diff --git a/samples/maildev-mailkit/package-lock.json b/samples/maildev-mailkit/package-lock.json new file mode 100644 index 000000000..9e1a8266e --- /dev/null +++ b/samples/maildev-mailkit/package-lock.json @@ -0,0 +1,1897 @@ +{ + "name": "aspire-maildev-apphost", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "aspire-maildev-apphost", + "version": "1.0.0", + "dependencies": { + "vscode-jsonrpc": "^8.2.1" + }, + "devDependencies": { + "@types/node": "^25.8.0", + "eslint": "^10.3.0", + "nodemon": "^3.1.14", + "tsx": "^4.22.0", + "typescript": "^6.0.3", + "typescript-eslint": "^8.59.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "integrity": "sha1-egGo0uwvuy2seK2tCbD6eB5Agr4=", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "integrity": "sha1-cEvSl95tdi3lTqu+r79V9nVqvi8=", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "integrity": "sha1-tUCifRTkr9BYSWpNvsTT9BTbEQo=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "integrity": "sha1-0csWbTSw+/D+irRgpVlPJKN4cB4=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "integrity": "sha1-EDSyZFf8iGNo/mG70J9lP2r6jlQ=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "integrity": "sha1-ZVVqQyoeTXIDLYIYwZMvzKGkl3I=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "integrity": "sha1-LmHgWS+QMNfj2uGO4l68U1kYrvY=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "integrity": "sha1-yV7CiZWe+AecTcqBeh4sS+Zrm9M=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "integrity": "sha1-wJoPZ5F1kqwN6JKpvk04FN69Kmw=", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "integrity": "sha1-QLIhdd2gYYLz7oFBGGxf8wTEpxc=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "integrity": "sha1-pYD5xnZ5eDOJHlGfx6EzfIr9jbM=", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "integrity": "sha1-RkUs8yHcf56Rwvp4Cla7Vuec1os=", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "integrity": "sha1-QhGzGE3WYI9T3LIuOfXTTuCIUsg=", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "integrity": "sha1-aXhXwqYcubC2u2ZS5AwdxeHKjl0=", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "integrity": "sha1-0ZKUPrFGpArExkl9DPe+NbmGvwg=", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "integrity": "sha1-rOoDVtoODrwI+Xz3ucLkAeHmSNw=", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "integrity": "sha1-bww84MtkxTS3DExF7LLBbTTjXf0=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "integrity": "sha1-i813B3oNzjN4tXT+2ybSolO3PTY=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "integrity": "sha1-5/sqAemcgwyU5mI82f77TI+1g0c=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "integrity": "sha1-xSkJNy24uG4sVeBaiUADO1Zgo7I=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "integrity": "sha1-xCe5vlpkwmL/mn63C1+7qt9EbGw=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "integrity": "sha1-3JsUe6yi5sSzyFVxdB70hgpIkJc=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "integrity": "sha1-zoZtEt8TwV5MmfBzo9Rm9uBkmzo=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "integrity": "sha1-dGjjaS0B1inVlB5dg4F7uA+eObQ=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "integrity": "sha1-pbwAY/sryrbQ7WPyoVN5WLwmnsY=", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "integrity": "sha1-EAZO5E9DR7kMmgK0Rrv4CpFjKxI=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "integrity": "sha1-iRG9crLDZApUNgngQAuMTS5+fLY=", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "integrity": "sha1-DNcv6FUOPC6uFWqWpN3c0cisWAA=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "integrity": "sha1-vM32Fbz3tujbgw7AuNIcmiXeWXs=", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "integrity": "sha1-VuhtJDBJGV2KzAwGobPf3D+j3pU=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "integrity": "sha1-Ce5KoHtz8FnsLUx0v0sv8CsyI3c=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "integrity": "sha1-wdp80bgvqHh/mLVin7gRhIobY84=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "integrity": "sha1-iOm/TRHSsZwILnjr586IckpesJE=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "integrity": "sha1-Swli8/LHzovJiz7P40UlwJ0styk=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "integrity": "sha1-qCcsoDsqz0kmcCIrIyC2xCG/3mA=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "integrity": "sha1-j4AMzME/T4zTEW4tnAqUk52j4+0=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "integrity": "sha1-8qCfYgEjkLK/8/xvskjd7IwJoJA=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "integrity": "sha1-r1smkaIrRL6EewyoFkHF+2rQFyw=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "integrity": "sha1-wrnS43TuYsWG062+qHGZsdenpro=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "integrity": "sha1-b2Nq+WL75hkbgwvWdrpZhpJrzOw=", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "integrity": "sha1-zz8Oh2177hWpOrkluCv1cKOQSiQ=", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "integrity": "sha1-WWoXRyM2lNUPatinhp/Lb1bPWEE=", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.5", + "integrity": "sha1-D+/Anm6C6UzeKRus9DUi6YnrAaQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.66.0", + "integrity": "sha1-duhqpaJFn79bvXqDnA3AzOHVYiQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/type-utils": "8.66.0", + "@typescript-eslint/utils": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.66.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "integrity": "sha1-aleq70yQ3yesNZCHXSno8RmIyI4=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.66.0", + "integrity": "sha1-iOOGXs9zsBGBNOfLgx2oepYaV6E=", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.66.0", + "integrity": "sha1-go94iJXfUtnrK1Q0RaOloT41q04=", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.66.0", + "@typescript-eslint/types": "^8.66.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.66.0", + "integrity": "sha1-T//Mbr0N+f55g8AlaWdWfqb1rGM=", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.66.0", + "integrity": "sha1-OokGbFB6owVB3BdoBGhbS0ROHlI=", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.66.0", + "integrity": "sha1-sjFTA+ynL62a+nvk9Y8FPI8qBHk=", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/utils": "8.66.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.66.0", + "integrity": "sha1-PKyrlNO1ZMHUjFbrN7ifiabUhHk=", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.66.0", + "integrity": "sha1-GjjDqX3Gxmm2bVhdf5DrxPuzKlA=", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.66.0", + "@typescript-eslint/tsconfig-utils": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.66.0", + "integrity": "sha1-4nfWdCcEPNyiWA7pGqYpIeRomWk=", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.66.0", + "integrity": "sha1-TElOlHRfsnJKTzejEAkeVrZE0Yo=", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.66.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "integrity": "sha1-T68BstbTJr/u2XrqH1IiC19MGUA=", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "integrity": "sha1-ftW7VZCLOy8bxVxq8WU7rafweTc=", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "integrity": "sha1-B+mCx0YmFnqnoklcU4F4ktcTlJI=", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "integrity": "sha1-eQxYsZuhcgqEIFtXxhjVrYUklz4=", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "integrity": "sha1-v7EGYv7tgZaixi58aOF3IMJ0F5o=", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "integrity": "sha1-9uFKl4WNMnJSIAJC1Mz+UixEVSI=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "integrity": "sha1-fHJDiAm1+lur9UGZofHCgaaYT88=", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "integrity": "sha1-SQMy9AkZRSJy1VqEgK3AxEE1h4k=", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "integrity": "sha1-GXxsxmnvKo3F57TZfuTgksPrDVs=", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "integrity": "sha1-hpgyxYA0/mikCTwX3BXoNA2EAcQ=", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "integrity": "sha1-ilj+ePANzXDDcEUXWd+/rwPo7p8=", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "integrity": "sha1-xq5DLZvZZiWC/OCHCbA4xY6ePWo=", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "integrity": "sha1-pvLc5hL63S7x9Rm3NVHxfoUZmDE=", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "integrity": "sha1-70W0Y0ycnZeilq6kEUpfmED5VXg=", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "integrity": "sha1-FLqDpdNz49MR5a/KKc9b+tllvzQ=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.8.0", + "integrity": "sha1-5tGZB6PwkKU6AiJhujTF/20EkIs=", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "integrity": "sha1-ud5qzi+rHP8k0uWNhbdMj86jmAI=", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "integrity": "sha1-njyUiWl4JNLUzjqK0SYo+R6fWb4=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "integrity": "sha1-AdXkfcMyqrowWQCDYkVKjMNMyqU=", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "integrity": "sha1-CNBI8mHw3e21uulfRoCUY9nJSW0=", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "integrity": "sha1-eteWTWeauyi+5yzsY3WLHF0smSE=", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "integrity": "sha1-LupSkHAvJquP5TcDcP+GyWXSESM=", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "integrity": "sha1-dNLrTeC42hKTcRkQ1Qd1ubcQ72Q=", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "integrity": "sha1-On1WtVnWy8PrUSMlJE5hmmXGxSU=", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "integrity": "sha1-h0v2nG9ATCtdmcSBNBOZ/VWJJjM=", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "integrity": "sha1-d4e93PETG/+5JjbGlFe7wO3W2B8=", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "integrity": "sha1-RCZdPKwH4+p9wkdRY4BkN1SgUpI=", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "integrity": "sha1-TJKBnstwg1YeT0okCoa+UZj1Nvw=", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "integrity": "sha1-Ds45/LFO4BL0sEEL0z3ZwfAREnw=", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "integrity": "sha1-ruyipQYwPwzuYcWebJ8qiNLyn8Y=", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "integrity": "sha1-ysZAd4XQNnWipeGlMFxpezR9kNY=", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "integrity": "sha1-bSN9mQg5UMeSkPJMdkKj3poo+eM=", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "integrity": "sha1-PNQOcp82Q/2HywTlC/DrcivFlvU=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", + "dev": true, + "license": "ISC" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "integrity": "sha1-6h9/O4DwZCNug0cPhsCcJU+0Wwk=", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "integrity": "sha1-ZPYeQsu7LuwgcanawLKLoeZdUIQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "integrity": "sha1-dTU0W4lnNNX4DE0GxQlVUnoU8Ss=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true, + "license": "ISC" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "integrity": "sha1-kziAKjDTtmBfvgYT4JQAjKjAWhM=", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "integrity": "sha1-qHmpnilFL5QkOfKkBeOvizHU3pM=", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "integrity": "sha1-rkViwAdHO5MqYgDUAyaN0v/8at4=", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "integrity": "sha1-VTIeswn+u8WcSAHZMackUqaB0oY=", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "integrity": "sha1-/ZVrvgt3JB6fFaxdzLHGOAYJaO8=", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "integrity": "sha1-V0yBOM4dK1hh8LRFedut1gxmFbI=", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon": { + "version": "3.1.14", + "integrity": "sha1-hIfKN5xRUwHSIewAfyfyTsr6K1E=", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "integrity": "sha1-Dc1p/yOhybEf0JeDFmRKA4ghamU=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "integrity": "sha1-fqHBpdkddk+yghOciP4R4YKjpzQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "integrity": "sha1-4drMvnjQ0TiMoYxk/qOOPlfjcGs=", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "integrity": "sha1-g8gxXGeFAF470CGDlBHJ4RDm2DQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "integrity": "sha1-UTvb4tO5XXdi6METfvoZXGxhtbM=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "integrity": "sha1-WB9q3mWMu6ZaDTOA3ndTKVBU83U=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.2", + "integrity": "sha1-WpQpFeJrNy3A8OZ1MUmhbmscVgE=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "integrity": "sha1-3rxkidem5rDnYRiIzsiAM30xY5Y=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "integrity": "sha1-wkIiT0pnwh9oaDm720rCgrg3PTo=", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "integrity": "sha1-AnQi4vrsCyXhVJw+G9gwm5EztuU=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "integrity": "sha1-dKNwvYVxFuJFspzJc0DNQxoCpsc=", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "integrity": "sha1-ObZGA33VDBT7RR5+TKxY7YuGP2k=", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "integrity": "sha1-zNCvT4g1+9wmW4JGGq8MNmY/NOo=", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "integrity": "sha1-rhbxZE2HPsrYQ7AwexQzYtTEIXI=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "integrity": "sha1-1wuSvat9bZDf1zkxGVowtuPXzrs=", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "integrity": "sha1-ViqabJ6ys7Ej05cZ+a9btE/NdjE=", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "integrity": "sha1-7Sq5Z6MxreYvGNB32uGSaE1Q01A=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.5", + "integrity": "sha1-UepXoX2G9gX4EDlZX7xA7QalX6s=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "integrity": "sha1-FkjESq58jZiKMmAY7XL1tN0DkuQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "integrity": "sha1-CXoj17FhR2Q15cE0SpXA91tKVpQ=", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "integrity": "sha1-Ss1KFV4ic0mQpe0f6el/ETvLN8E=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsx": { + "version": "4.23.11", + "integrity": "sha1-gqfLzOFntXUGDbA+94moTGh8f5c=", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "integrity": "sha1-B7ggO/pwVsBlcFDjzNLDdzC6uPE=", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "integrity": "sha1-kCUdwAeRbpcnhsuU100VsYVXfSE=", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.66.0", + "integrity": "sha1-CAm20lyKCSRpC6MNwfBWBwk8Efs=", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.66.0", + "@typescript-eslint/parser": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/utils": "8.66.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "integrity": "sha1-OHM7kye9zSJtuIn7cjpu/RYubiw=", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.24.6", + "integrity": "sha1-YSdbSF1/1OnSacfPBOwoc8nMD5E=", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "integrity": "sha1-mxpSWVIlhZ5V9mnZKPiMbFfyp34=", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.1", + "integrity": "sha1-oyLMDx2X95T/2cTNKomKC94JfzQ=", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "integrity": "sha1-fGqN0KY2oDJ+ELWckobu6T8/UbE=", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "integrity": "sha1-0sRcbdT7zmIaZvE2y+Mor9BBCzQ=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "integrity": "sha1-ApTrPe4FAo0x7hpfosVWpqrxChs=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/samples/maildev-mailkit/package.json b/samples/maildev-mailkit/package.json new file mode 100644 index 000000000..f794f797f --- /dev/null +++ b/samples/maildev-mailkit/package.json @@ -0,0 +1,26 @@ +{ + "name": "aspire-maildev-apphost", + "version": "1.0.0", + "private": true, + "type": "module", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "scripts": { + "aspire:lint": "eslint apphost.mts", + "aspire:start": "aspire run", + "aspire:build": "tsc -p tsconfig.apphost.json", + "aspire:dev": "tsc --watch -p tsconfig.apphost.json" + }, + "dependencies": { + "vscode-jsonrpc": "^8.2.1" + }, + "devDependencies": { + "@types/node": "^25.8.0", + "eslint": "^10.3.0", + "nodemon": "^3.1.14", + "tsx": "^4.22.0", + "typescript": "^6.0.3", + "typescript-eslint": "^8.59.3" + } +} \ No newline at end of file diff --git a/samples/maildev-mailkit/tsconfig.apphost.json b/samples/maildev-mailkit/tsconfig.apphost.json new file mode 100644 index 000000000..139b67749 --- /dev/null +++ b/samples/maildev-mailkit/tsconfig.apphost.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "outDir": "./dist/apphost", + "rootDir": "." + }, + "include": ["apphost.mts", ".aspire/modules/**/*.mts"], + "exclude": ["node_modules"] +} \ No newline at end of file