From 0a5943f73f3c2a3c845a298a6b6c90f16ee6ef6a Mon Sep 17 00:00:00 2001 From: David Fowler Date: Thu, 26 Mar 2026 22:51:50 -0700 Subject: [PATCH] Move Python starter template to TypeScript AppHost and CLI template factory Migrate the aspire-py-starter template from the legacy DotNetTemplateFactory (C# AppHost, dotnet new) to the CliTemplateFactory (TypeScript AppHost, embedded resources). This aligns the Python starter with the existing TypeScript Express/React starter pattern. Changes: - Add py-starter embedded template with TypeScript AppHost using addUvicornApp and addViteApp - Add CliTemplateFactory.PythonStarterTemplate with Redis conditional support - Add ConditionalBlockProcessor for reusable {{#condition}} block processing - Remove old template from DotNetTemplateFactory and Aspire.ProjectTemplates - Install uv in E2E Docker container for Python template support - Fix AspireStartAsync sed pattern to match both http and https dashboard URLs - Add workspace and log capture diagnostics for E2E test debugging Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/ci-test-failures/SKILL.md | 14 +- .github/skills/cli-e2e-testing/SKILL.md | 28 +- src/Aspire.Cli/Aspire.Cli.csproj | 3 + ...liTemplateFactory.PythonStarterTemplate.cs | 160 ++ .../Templating/CliTemplateFactory.cs | 20 +- .../Templating/ConditionalBlockProcessor.cs | 105 + .../Templating/DotNetTemplateFactory.cs | 21 - src/Aspire.Cli/Templating/KnownTemplateId.cs | 5 + .../Templates/py-starter}/app/.dockerignore | 0 .../Templates/py-starter}/app/.python-version | 0 .../Templates/py-starter}/app/main.py | 30 +- .../Templates/py-starter}/app/pyproject.toml | 6 +- .../Templates/py-starter}/app/telemetry.py | 0 .../Templates/py-starter/apphost.ts | 31 + .../Templates/py-starter/aspire.config.json | 27 + .../Templates/py-starter/eslint.config.mjs | 17 + .../py-starter}/frontend/.dockerignore | 0 .../Templates/py-starter}/frontend/.gitignore | 0 .../py-starter}/frontend/eslint.config.js | 0 .../Templates/py-starter}/frontend/index.html | 0 .../py-starter}/frontend/package-lock.json | 0 .../py-starter}/frontend/package.json | 0 .../py-starter}/frontend/public/Aspire.png | Bin .../py-starter}/frontend/public/github.svg | 0 .../py-starter}/frontend/src/App.css | 0 .../py-starter}/frontend/src/App.tsx | 0 .../py-starter}/frontend/src/index.css | 0 .../py-starter}/frontend/src/main.tsx | 0 .../py-starter}/frontend/src/vite-env.d.ts | 0 .../py-starter}/frontend/tsconfig.app.json | 0 .../py-starter}/frontend/tsconfig.json | 0 .../py-starter}/frontend/tsconfig.node.json | 0 .../py-starter}/frontend/vite.config.ts | 0 .../Templates/py-starter/package-lock.json | 2042 +++++++++++++++++ .../Templates/py-starter/package.json | 27 + .../Templates/py-starter/tsconfig.json | 8 + .../Aspire.ProjectTemplates.csproj | 8 +- .../.template.config/dotnetcli.host.json | 18 - .../.template.config/ide.host.json | 20 - .../localize/templatestrings.cs.json | 16 - .../localize/templatestrings.de.json | 16 - .../localize/templatestrings.en.json | 16 - .../localize/templatestrings.es.json | 16 - .../localize/templatestrings.fr.json | 16 - .../localize/templatestrings.it.json | 16 - .../localize/templatestrings.ja.json | 16 - .../localize/templatestrings.ko.json | 16 - .../localize/templatestrings.pl.json | 16 - .../localize/templatestrings.pt-BR.json | 16 - .../localize/templatestrings.ru.json | 16 - .../localize/templatestrings.tr.json | 16 - .../localize/templatestrings.zh-Hans.json | 16 - .../localize/templatestrings.zh-Hant.json | 16 - .../.template.config/template.json | 245 -- .../templates/aspire-py-starter/apphost.cs | 29 - .../aspire-py-starter/apphost.run.json | 39 - .../CaptureWorkspaceOnFailureAttribute.cs | 62 + .../Helpers/CliE2EAutomatorHelpers.cs | 44 +- .../Helpers/CliE2ETestHelpers.cs | 54 + .../PythonReactTemplateTests.cs | 16 +- .../ConditionalBlockProcessorTests.cs | 457 ++++ .../Templating/DotNetTemplateFactoryTests.cs | 2 +- .../LocalhostTldHostnameTests.cs | 5 +- tests/Shared/Docker/Dockerfile.e2e | 4 +- tests/Shared/TemporaryRepo.cs | 3 + 65 files changed, 3136 insertions(+), 638 deletions(-) create mode 100644 src/Aspire.Cli/Templating/CliTemplateFactory.PythonStarterTemplate.cs create mode 100644 src/Aspire.Cli/Templating/ConditionalBlockProcessor.cs rename src/{Aspire.ProjectTemplates/templates/aspire-py-starter => Aspire.Cli/Templating/Templates/py-starter}/app/.dockerignore (100%) rename src/{Aspire.ProjectTemplates/templates/aspire-py-starter => Aspire.Cli/Templating/Templates/py-starter}/app/.python-version (100%) rename src/{Aspire.ProjectTemplates/templates/aspire-py-starter => Aspire.Cli/Templating/Templates/py-starter}/app/main.py (94%) rename src/{Aspire.ProjectTemplates/templates/aspire-py-starter => Aspire.Cli/Templating/Templates/py-starter}/app/pyproject.toml (92%) rename src/{Aspire.ProjectTemplates/templates/aspire-py-starter => Aspire.Cli/Templating/Templates/py-starter}/app/telemetry.py (100%) create mode 100644 src/Aspire.Cli/Templating/Templates/py-starter/apphost.ts create mode 100644 src/Aspire.Cli/Templating/Templates/py-starter/aspire.config.json create mode 100644 src/Aspire.Cli/Templating/Templates/py-starter/eslint.config.mjs rename src/{Aspire.ProjectTemplates/templates/aspire-py-starter => Aspire.Cli/Templating/Templates/py-starter}/frontend/.dockerignore (100%) rename src/{Aspire.ProjectTemplates/templates/aspire-py-starter => Aspire.Cli/Templating/Templates/py-starter}/frontend/.gitignore (100%) rename src/{Aspire.ProjectTemplates/templates/aspire-py-starter => Aspire.Cli/Templating/Templates/py-starter}/frontend/eslint.config.js (100%) rename src/{Aspire.ProjectTemplates/templates/aspire-py-starter => Aspire.Cli/Templating/Templates/py-starter}/frontend/index.html (100%) rename src/{Aspire.ProjectTemplates/templates/aspire-py-starter => Aspire.Cli/Templating/Templates/py-starter}/frontend/package-lock.json (100%) rename src/{Aspire.ProjectTemplates/templates/aspire-py-starter => Aspire.Cli/Templating/Templates/py-starter}/frontend/package.json (100%) rename src/{Aspire.ProjectTemplates/templates/aspire-py-starter => Aspire.Cli/Templating/Templates/py-starter}/frontend/public/Aspire.png (100%) rename src/{Aspire.ProjectTemplates/templates/aspire-py-starter => Aspire.Cli/Templating/Templates/py-starter}/frontend/public/github.svg (100%) rename src/{Aspire.ProjectTemplates/templates/aspire-py-starter => Aspire.Cli/Templating/Templates/py-starter}/frontend/src/App.css (100%) rename src/{Aspire.ProjectTemplates/templates/aspire-py-starter => Aspire.Cli/Templating/Templates/py-starter}/frontend/src/App.tsx (100%) rename src/{Aspire.ProjectTemplates/templates/aspire-py-starter => Aspire.Cli/Templating/Templates/py-starter}/frontend/src/index.css (100%) rename src/{Aspire.ProjectTemplates/templates/aspire-py-starter => Aspire.Cli/Templating/Templates/py-starter}/frontend/src/main.tsx (100%) rename src/{Aspire.ProjectTemplates/templates/aspire-py-starter => Aspire.Cli/Templating/Templates/py-starter}/frontend/src/vite-env.d.ts (100%) rename src/{Aspire.ProjectTemplates/templates/aspire-py-starter => Aspire.Cli/Templating/Templates/py-starter}/frontend/tsconfig.app.json (100%) rename src/{Aspire.ProjectTemplates/templates/aspire-py-starter => Aspire.Cli/Templating/Templates/py-starter}/frontend/tsconfig.json (100%) rename src/{Aspire.ProjectTemplates/templates/aspire-py-starter => Aspire.Cli/Templating/Templates/py-starter}/frontend/tsconfig.node.json (100%) rename src/{Aspire.ProjectTemplates/templates/aspire-py-starter => Aspire.Cli/Templating/Templates/py-starter}/frontend/vite.config.ts (100%) create mode 100644 src/Aspire.Cli/Templating/Templates/py-starter/package-lock.json create mode 100644 src/Aspire.Cli/Templating/Templates/py-starter/package.json create mode 100644 src/Aspire.Cli/Templating/Templates/py-starter/tsconfig.json delete mode 100644 src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/dotnetcli.host.json delete mode 100644 src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/ide.host.json delete mode 100644 src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.cs.json delete mode 100644 src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.de.json delete mode 100644 src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.en.json delete mode 100644 src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.es.json delete mode 100644 src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.fr.json delete mode 100644 src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.it.json delete mode 100644 src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.ja.json delete mode 100644 src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.ko.json delete mode 100644 src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.pl.json delete mode 100644 src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.pt-BR.json delete mode 100644 src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.ru.json delete mode 100644 src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.tr.json delete mode 100644 src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.zh-Hans.json delete mode 100644 src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.zh-Hant.json delete mode 100644 src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/template.json delete mode 100644 src/Aspire.ProjectTemplates/templates/aspire-py-starter/apphost.cs delete mode 100644 src/Aspire.ProjectTemplates/templates/aspire-py-starter/apphost.run.json create mode 100644 tests/Aspire.Cli.EndToEnd.Tests/Helpers/CaptureWorkspaceOnFailureAttribute.cs create mode 100644 tests/Aspire.Cli.Tests/Templating/ConditionalBlockProcessorTests.cs diff --git a/.github/skills/ci-test-failures/SKILL.md b/.github/skills/ci-test-failures/SKILL.md index ad2213915e1..b7f5144bb5f 100644 --- a/.github/skills/ci-test-failures/SKILL.md +++ b/.github/skills/ci-test-failures/SKILL.md @@ -214,11 +214,23 @@ artifact_0_TestName_os/ ├── testresults/ │ ├── TestName_net10.0_timestamp.trx # Test results XML │ ├── Aspire.*.Tests_*.log # Console output -│ └── recordings/ # Asciinema recordings (CLI E2E tests) +│ ├── recordings/ # Asciinema recordings (CLI E2E tests) +│ └── workspaces/ # Captured project workspaces (CLI E2E tests) +│ └── TestClassName.MethodName/ # Full generated project for failed tests +│ ├── apphost.ts +│ ├── aspire.config.json +│ ├── .modules/ # Generated SDK (aspire.js) - key for debugging +│ └── ... ├── *.crash.dmp # Crash dump (if test crashed) └── test.binlog # MSBuild binary log ``` +### CLI E2E Workspace Capture + +CLI E2E tests annotated with `[CaptureWorkspaceOnFailure]` automatically capture the full generated project workspace when a test fails. This includes the generated SDK (`.modules/aspire.js`), template output, and config files — critical for debugging template generation or `aspire run` failures. + +Look in `testresults/workspaces/{TestClassName.MethodName}/` inside the downloaded artifact. + ## Parsing .trx Files ```powershell diff --git a/.github/skills/cli-e2e-testing/SKILL.md b/.github/skills/cli-e2e-testing/SKILL.md index b4cacde0f02..35fcf4f18e6 100644 --- a/.github/skills/cli-e2e-testing/SKILL.md +++ b/.github/skills/cli-e2e-testing/SKILL.md @@ -569,9 +569,31 @@ testresults/ ├── Aspire.Cli.EndToEnd.Tests_*.log # Console output log ├── *.crash.dmp # Crash dump (if test crashed) ├── test.binlog # MSBuild binary log -└── recordings/ - ├── CreateAndRunAspireStarterProject.cast # Asciinema recording - └── ... +├── recordings/ +│ ├── CreateAndRunAspireStarterProject.cast # Asciinema recording +│ └── ... +└── workspaces/ # Captured project workspaces (on failure) + └── TestClassName.MethodName/ # Full generated project for debugging + ├── apphost.ts + ├── aspire.config.json + ├── .modules/ # Generated SDK - check aspire.js for exports + └── ... +``` + +### Workspace Capture + +Tests annotated with `[CaptureWorkspaceOnFailure]` automatically copy the generated project workspace into the test artifacts when a test fails. This is invaluable for debugging template generation or `aspire run` failures — you can inspect the exact generated files including the SDK output in `.modules/aspire.js`. + +To add workspace capture to a new test: +```csharp +[Fact] +[CaptureWorkspaceOnFailure] +public async Task MyTemplateTest() +{ + var workspace = TemporaryWorkspace.Create(output); + // ... test code — workspace is automatically registered for capture ... +} +``` ``` ### One-Liner: Download Latest Recording diff --git a/src/Aspire.Cli/Aspire.Cli.csproj b/src/Aspire.Cli/Aspire.Cli.csproj index 8a59cc48f27..109d9009369 100644 --- a/src/Aspire.Cli/Aspire.Cli.csproj +++ b/src/Aspire.Cli/Aspire.Cli.csproj @@ -212,6 +212,9 @@ false + + false + diff --git a/src/Aspire.Cli/Templating/CliTemplateFactory.PythonStarterTemplate.cs b/src/Aspire.Cli/Templating/CliTemplateFactory.PythonStarterTemplate.cs new file mode 100644 index 00000000000..7af3c455774 --- /dev/null +++ b/src/Aspire.Cli/Templating/CliTemplateFactory.PythonStarterTemplate.cs @@ -0,0 +1,160 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Cli.Configuration; +using Aspire.Cli.Interaction; +using Aspire.Cli.Projects; +using Aspire.Cli.Resources; +using Microsoft.Extensions.Logging; +using Spectre.Console; + +namespace Aspire.Cli.Templating; + +internal sealed partial class CliTemplateFactory +{ + private async Task ApplyPythonStarterTemplateAsync(CallbackTemplate _, TemplateInputs inputs, System.CommandLine.ParseResult parseResult, CancellationToken cancellationToken) + { + var projectName = inputs.Name; + if (string.IsNullOrWhiteSpace(projectName)) + { + var defaultName = _executionContext.WorkingDirectory.Name; + projectName = await _prompter.PromptForProjectNameAsync(defaultName, cancellationToken); + } + + if (string.IsNullOrWhiteSpace(inputs.Version)) + { + _interactionService.DisplayError("Unable to determine Aspire version for the Python starter template."); + return new TemplateResult(ExitCodeConstants.InvalidCommand); + } + + var aspireVersion = inputs.Version; + var outputPath = inputs.Output; + if (string.IsNullOrWhiteSpace(outputPath)) + { + var defaultOutputPath = $"./{projectName}"; + outputPath = await _prompter.PromptForOutputPath(defaultOutputPath, cancellationToken); + } + outputPath = Path.GetFullPath(outputPath, _executionContext.WorkingDirectory.FullName); + + _logger.LogDebug("Applying Python starter template. ProjectName: {ProjectName}, OutputPath: {OutputPath}, AspireVersion: {AspireVersion}.", projectName, outputPath, aspireVersion); + + var useLocalhostTld = await ResolveUseLocalhostTldAsync(parseResult, cancellationToken); + var useRedisCache = await ResolveUseRedisCacheAsync(parseResult, cancellationToken); + + TemplateResult templateResult; + try + { + if (!Directory.Exists(outputPath)) + { + Directory.CreateDirectory(outputPath); + } + + templateResult = await _interactionService.ShowStatusAsync( + TemplatingStrings.CreatingNewProject, + async () => + { + var projectNameLower = projectName.ToLowerInvariant(); + + var ports = GenerateRandomPorts(); + var hostName = useLocalhostTld ? $"{projectNameLower}.dev.localhost" : "localhost"; + var conditions = new Dictionary + { + ["redis"] = useRedisCache, + ["no-redis"] = !useRedisCache, + }; + string ApplyAllTokens(string content) => ConditionalBlockProcessor.Process( + ApplyTokens(content, projectName, projectNameLower, aspireVersion, ports, hostName), + conditions); + _logger.LogDebug("Copying embedded Python starter template files to '{OutputPath}'.", outputPath); + await CopyTemplateTreeToDiskAsync("py-starter", outputPath, ApplyAllTokens, cancellationToken); + + if (useRedisCache) + { + AddRedisPackageToConfig(outputPath, aspireVersion); + } + + // Write channel to settings.json before restore so package resolution uses the selected channel. + if (!string.IsNullOrEmpty(inputs.Channel)) + { + var config = AspireJsonConfiguration.Load(outputPath); + if (config is not null) + { + config.Channel = inputs.Channel; + config.Save(outputPath); + } + } + + var appHostProject = _projectFactory.TryGetProject(new FileInfo(Path.Combine(outputPath, "apphost.ts"))); + if (appHostProject is not IGuestAppHostSdkGenerator guestProject) + { + _interactionService.DisplayError("Automatic 'aspire restore' is unavailable for the new Python starter project because no TypeScript AppHost SDK generator was found."); + return new TemplateResult(ExitCodeConstants.FailedToBuildArtifacts, outputPath); + } + + _logger.LogDebug("Generating SDK code for Python starter in '{OutputPath}'.", outputPath); + var restoreSucceeded = await guestProject.BuildAndGenerateSdkAsync(new DirectoryInfo(outputPath), cancellationToken); + if (!restoreSucceeded) + { + _interactionService.DisplayError("Automatic 'aspire restore' failed for the new Python starter project. Run 'aspire restore' in the project directory for more details."); + return new TemplateResult(ExitCodeConstants.FailedToBuildArtifacts, outputPath); + } + + return new TemplateResult(ExitCodeConstants.Success, outputPath); + }, emoji: KnownEmojis.Rocket); + + if (templateResult.ExitCode != ExitCodeConstants.Success) + { + return templateResult; + } + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + _interactionService.DisplayError($"Failed to create project files: {ex.Message}"); + return new TemplateResult(ExitCodeConstants.FailedToCreateNewProject); + } + + _interactionService.DisplaySuccess($"Created Python starter project at {outputPath.EscapeMarkup()}"); + DisplayPostCreationInstructions(outputPath); + + return templateResult; + } + + private async Task ResolveUseRedisCacheAsync(System.CommandLine.ParseResult parseResult, CancellationToken cancellationToken) + { + var redisCacheOptionSpecified = parseResult.Tokens.Any(token => + string.Equals(token.Value, "--use-redis-cache", StringComparisons.CliInputOrOutput)); + var useRedisCache = parseResult.GetValue(_useRedisCacheOption); + if (!redisCacheOptionSpecified) + { + if (!_hostEnvironment.SupportsInteractiveInput) + { + return false; + } + + useRedisCache = await _interactionService.PromptForSelectionAsync( + TemplatingStrings.UseRedisCache_Prompt, + [TemplatingStrings.Yes, TemplatingStrings.No], + choice => choice, + cancellationToken) switch + { + var choice when string.Equals(choice, TemplatingStrings.Yes, StringComparisons.CliInputOrOutput) => true, + var choice when string.Equals(choice, TemplatingStrings.No, StringComparisons.CliInputOrOutput) => false, + _ => throw new InvalidOperationException(TemplatingStrings.UseRedisCache_UnexpectedChoice) + }; + } + + if (useRedisCache ?? false) + { + _interactionService.DisplayMessage(KnownEmojis.CheckMark, TemplatingStrings.UseRedisCache_UsingRedisCache); + } + + return useRedisCache ?? false; + } + + private static void AddRedisPackageToConfig(string outputPath, string aspireVersion) + { + var config = AspireConfigFile.LoadOrCreate(outputPath); + config.AddOrUpdatePackage("Aspire.Hosting.Redis", aspireVersion); + config.Save(outputPath); + } +} diff --git a/src/Aspire.Cli/Templating/CliTemplateFactory.cs b/src/Aspire.Cli/Templating/CliTemplateFactory.cs index 2df9982de00..f5e89c4cce1 100644 --- a/src/Aspire.Cli/Templating/CliTemplateFactory.cs +++ b/src/Aspire.Cli/Templating/CliTemplateFactory.cs @@ -37,6 +37,11 @@ internal sealed partial class CliTemplateFactory : ITemplateFactory Description = TemplatingStrings.UseLocalhostTld_Description }; + private readonly Option _useRedisCacheOption = new("--use-redis-cache") + { + Description = TemplatingStrings.UseRedisCache_Description + }; + private readonly ILanguageDiscovery _languageDiscovery; private readonly IAppHostProjectFactory _projectFactory; private readonly IScaffoldingService _scaffoldingService; @@ -130,7 +135,20 @@ private IEnumerable GetTemplateDefinitions() ApplyEmptyAppHostTemplateAsync, runtime: TemplateRuntime.Cli, languageId: KnownLanguageId.Java, - isEmpty: true) + isEmpty: true), + + new CallbackTemplate( + KnownTemplateId.PythonStarter, + "Starter App (FastAPI/React)", + projectName => $"./{projectName}", + cmd => + { + AddOptionIfMissing(cmd, _localhostTldOption); + AddOptionIfMissing(cmd, _useRedisCacheOption); + }, + ApplyPythonStarterTemplateAsync, + runtime: TemplateRuntime.Cli, + languageId: KnownLanguageId.TypeScript) ]; return templates.Where(IsTemplateAvailable); diff --git a/src/Aspire.Cli/Templating/ConditionalBlockProcessor.cs b/src/Aspire.Cli/Templating/ConditionalBlockProcessor.cs new file mode 100644 index 00000000000..cc8c96973aa --- /dev/null +++ b/src/Aspire.Cli/Templating/ConditionalBlockProcessor.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Text.RegularExpressions; + +namespace Aspire.Cli.Templating; + +/// +/// Processes conditional blocks in template content. Blocks are delimited by +/// marker lines of the form {{#name}} / {{/name}}. When a block +/// is included, the marker lines are stripped and the inner content is kept; +/// when excluded, the marker lines and their content are removed entirely. +/// Marker lines may contain leading comment characters (e.g. // {{#name}} +/// or # {{#name}}) — the entire line is always removed. +/// +/// +/// Blocks must not overlap or nest across different condition names. Each condition +/// is processed independently in enumeration order. Overlapping blocks produce +/// undefined behavior. +/// +internal static partial class ConditionalBlockProcessor +{ + /// + /// Processes all conditional blocks for the given set of conditions. Each entry + /// in maps a block name to whether it should be included. + /// + /// The template content to process. + /// A set of block-name to include/exclude mappings. + /// The processed content with conditional blocks resolved. + internal static string Process(string content, IReadOnlyDictionary conditions) + { + foreach (var (blockName, include) in conditions) + { + content = ProcessBlock(content, blockName, include); + } + + Debug.Assert( + !LeftoverMarkerPattern().IsMatch(content), + $"Template content contains unprocessed conditional markers. Ensure all block names are included in the conditions dictionary."); + + return content; + } + + [GeneratedRegex(@"\{\{[#/][a-zA-Z][\w-]*\}\}")] + private static partial Regex LeftoverMarkerPattern(); + + /// + /// Processes all occurrences of a single conditional block in the content. + /// + /// The template content to process. + /// The name of the conditional block (e.g. redis). + /// + /// When , the block content is kept and only the marker lines + /// are removed. When , the entire block (markers and content) is removed. + /// + /// The processed content. + internal static string ProcessBlock(string content, string blockName, bool include) + { + var startPattern = $"{{{{#{blockName}}}}}"; + var endPattern = $"{{{{/{blockName}}}}}"; + + while (true) + { + var startIdx = content.IndexOf(startPattern, StringComparison.Ordinal); + if (startIdx < 0) + { + break; + } + + var endIdx = content.IndexOf(endPattern, startIdx, StringComparison.Ordinal); + if (endIdx < 0) + { + throw new InvalidOperationException( + $"Template contains opening marker '{{{{#{blockName}}}}}' without a matching closing marker '{{{{/{blockName}}}}}'."); + } + + // Find the full start marker line (including leading whitespace/comments and trailing newline). + var startLineBegin = content.LastIndexOf('\n', startIdx); + startLineBegin = startLineBegin < 0 ? 0 : startLineBegin + 1; + var startLineEnd = content.IndexOf('\n', startIdx); + startLineEnd = startLineEnd < 0 ? content.Length : startLineEnd + 1; + + // Find the full end marker line. + var endLineBegin = content.LastIndexOf('\n', endIdx); + endLineBegin = endLineBegin < 0 ? 0 : endLineBegin + 1; + var endLineEnd = content.IndexOf('\n', endIdx); + endLineEnd = endLineEnd < 0 ? content.Length : endLineEnd + 1; + + if (include) + { + // Keep the block content but remove the marker lines. + var blockContent = content[startLineEnd..endLineBegin]; + content = string.Concat(content.AsSpan(0, startLineBegin), blockContent, content.AsSpan(endLineEnd)); + } + else + { + // Remove everything from start marker line to end marker line (inclusive). + content = string.Concat(content.AsSpan(0, startLineBegin), content.AsSpan(endLineEnd)); + } + } + + return content; + } +} diff --git a/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs b/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs index 3e0fd548a2b..a4788202094 100644 --- a/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs +++ b/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs @@ -158,17 +158,6 @@ private IEnumerable GetTemplatesCore(bool showAllTemplates, bool nonI languageId: KnownLanguageId.CSharp ); - yield return new CallbackTemplate( - "aspire-py-starter", - TemplatingStrings.AspirePyStarter_Description, - projectName => $"./{projectName}", - ApplyDevLocalhostTldOption, - nonInteractive - ? ApplySingleFileTemplateWithNoExtraArgsAsync - : (template, inputs, parseResult, ct) => ApplySingleFileTemplate(template, inputs, parseResult, PromptForExtraAspirePythonStarterOptionsAsync, ct), - languageId: KnownLanguageId.CSharp - ); - if (showAllTemplates) { yield return new CallbackTemplate( @@ -290,16 +279,6 @@ private async Task PromptForExtraAspireSingleFileOptionsAsync(ParseRes return extraArgs.ToArray(); } - private async Task PromptForExtraAspirePythonStarterOptionsAsync(ParseResult result, CancellationToken cancellationToken) - { - var extraArgs = new List(); - - await PromptForDevLocalhostTldOptionAsync(result, extraArgs, cancellationToken); - await PromptForRedisCacheOptionAsync(result, extraArgs, cancellationToken); - - return extraArgs.ToArray(); - } - private async Task PromptForExtraAspireJsFrontendStarterOptionsAsync(ParseResult result, CancellationToken cancellationToken) { var extraArgs = new List(); diff --git a/src/Aspire.Cli/Templating/KnownTemplateId.cs b/src/Aspire.Cli/Templating/KnownTemplateId.cs index 36216035a26..bf1efd806c7 100644 --- a/src/Aspire.Cli/Templating/KnownTemplateId.cs +++ b/src/Aspire.Cli/Templating/KnownTemplateId.cs @@ -32,4 +32,9 @@ internal static class KnownTemplateId /// The template ID for the CLI Java empty AppHost template. /// public const string JavaEmptyAppHost = "aspire-java-empty"; + + /// + /// The template ID for the Python starter template. + /// + public const string PythonStarter = "aspire-py-starter"; } diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/app/.dockerignore b/src/Aspire.Cli/Templating/Templates/py-starter/app/.dockerignore similarity index 100% rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/app/.dockerignore rename to src/Aspire.Cli/Templating/Templates/py-starter/app/.dockerignore diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/app/.python-version b/src/Aspire.Cli/Templating/Templates/py-starter/app/.python-version similarity index 100% rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/app/.python-version rename to src/Aspire.Cli/Templating/Templates/py-starter/app/.python-version diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/app/main.py b/src/Aspire.Cli/Templating/Templates/py-starter/app/main.py similarity index 94% rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/app/main.py rename to src/Aspire.Cli/Templating/Templates/py-starter/app/main.py index 4d0a2a5d1c4..572643d6d2f 100644 --- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/app/main.py +++ b/src/Aspire.Cli/Templating/Templates/py-starter/app/main.py @@ -1,8 +1,8 @@ import contextlib import datetime -//#if UseRedisCache +# {{#redis}} import json -//#endif +# {{/redis}} import logging import os import random @@ -11,10 +11,10 @@ import fastapi.responses import fastapi.staticfiles import opentelemetry.instrumentation.fastapi as otel_fastapi -//#if UseRedisCache +# {{#redis}} import opentelemetry.instrumentation.redis as otel_redis import redis -//#endif +# {{/redis}} import telemetry @@ -27,7 +27,7 @@ async def lifespan(app): app = fastapi.FastAPI(lifespan=lifespan) otel_fastapi.FastAPIInstrumentor.instrument_app(app, exclude_spans=["send"]) -//#if UseRedisCache +# {{#redis}} # Create a global to store the Redis client. redis_client = None otel_redis.RedisInstrumentor().instrument() @@ -54,7 +54,7 @@ def get_redis_client(): ) return redis_client -//#endif +# {{/redis}} logger = logging.getLogger(__name__) @@ -66,7 +66,7 @@ async def root(): return "API service is running. Navigate to /api/weatherforecast to see sample data." @app.get("/api/weatherforecast") -//#if UseRedisCache +# {{#redis}} async def weather_forecast(redis_client=fastapi.Depends(get_redis_client)): """Weather forecast endpoint.""" cache_key = "weatherforecast" @@ -82,10 +82,11 @@ async def weather_forecast(redis_client=fastapi.Depends(get_redis_client)): except Exception as e: logger.warning(f"Redis cache read error: {e}") -//#else +# {{/redis}} +# {{#no-redis}} async def weather_forecast(): """Weather forecast endpoint.""" -//#endif +# {{/no-redis}} # Generate fresh data if not in cache or cache unavailable. summaries = [ "Freezing", @@ -112,7 +113,7 @@ async def weather_forecast(): } forecast.append(forecast_item) -//#if UseRedisCache +# {{#redis}} # Cache the data if redis_client: try: @@ -120,20 +121,21 @@ async def weather_forecast(): except Exception as e: logger.warning(f"Redis cache write error: {e}") -//#endif +# {{/redis}} return forecast @app.get("/health", response_class=fastapi.responses.PlainTextResponse) -//#if UseRedisCache +# {{#redis}} async def health_check(redis_client=fastapi.Depends(get_redis_client)): """Health check endpoint.""" if redis_client: redis_client.ping() -//#else +# {{/redis}} +# {{#no-redis}} async def health_check(): """Health check endpoint.""" -//#endif +# {{/no-redis}} return "Healthy" diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/app/pyproject.toml b/src/Aspire.Cli/Templating/Templates/py-starter/app/pyproject.toml similarity index 92% rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/app/pyproject.toml rename to src/Aspire.Cli/Templating/Templates/py-starter/app/pyproject.toml index c8f2d9e1138..5f47caff95f 100644 --- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/app/pyproject.toml +++ b/src/Aspire.Cli/Templating/Templates/py-starter/app/pyproject.toml @@ -8,8 +8,8 @@ dependencies = [ "opentelemetry-distro>=0.59b0", "opentelemetry-exporter-otlp-proto-grpc>=1.38.0", "opentelemetry-instrumentation-fastapi>=0.59b0", -//#if UseRedisCache +# {{#redis}} "opentelemetry-instrumentation-redis>=0.59b0", "redis>=6.4.0", -//#endif -] \ No newline at end of file +# {{/redis}} +] diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/app/telemetry.py b/src/Aspire.Cli/Templating/Templates/py-starter/app/telemetry.py similarity index 100% rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/app/telemetry.py rename to src/Aspire.Cli/Templating/Templates/py-starter/app/telemetry.py diff --git a/src/Aspire.Cli/Templating/Templates/py-starter/apphost.ts b/src/Aspire.Cli/Templating/Templates/py-starter/apphost.ts new file mode 100644 index 00000000000..e51954bafab --- /dev/null +++ b/src/Aspire.Cli/Templating/Templates/py-starter/apphost.ts @@ -0,0 +1,31 @@ +import { createBuilder } from './.modules/aspire.js'; + +const builder = await createBuilder(); + +// {{#redis}} +// Add a Redis cache for the app to use. +const cache = await builder + .addRedis("cache"); + +// {{/redis}} +// Run the Python FastAPI app and expose its HTTP endpoint externally. +const app = await builder + .addUvicornApp("app", "./app", "main:app") + .withUv() + .withExternalHttpEndpoints() +// {{#redis}} + .withReference(cache) + .waitFor(cache) +// {{/redis}} + .withHttpHealthCheck("/health"); + +// Run the Vite frontend after the API and inject the API URL for local proxying. +const frontend = await builder + .addViteApp("frontend", "./frontend") + .withReference(app) + .waitFor(app); + +// Bundle the frontend build output into the API container for publish/deploy. +await app.publishWithContainerFiles(frontend, "./static"); + +await builder.build().run(); diff --git a/src/Aspire.Cli/Templating/Templates/py-starter/aspire.config.json b/src/Aspire.Cli/Templating/Templates/py-starter/aspire.config.json new file mode 100644 index 00000000000..89488be7ad4 --- /dev/null +++ b/src/Aspire.Cli/Templating/Templates/py-starter/aspire.config.json @@ -0,0 +1,27 @@ +{ + "appHost": { + "path": "apphost.ts", + "language": "typescript/nodejs" + }, + "packages": { + "Aspire.Hosting.JavaScript": "{{aspireVersion}}", + "Aspire.Hosting.Python": "{{aspireVersion}}" + }, + "profiles": { + "https": { + "applicationUrl": "https://{{hostName}}:{{httpsPort}};http://{{hostName}}:{{httpPort}}", + "environmentVariables": { + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://{{hostName}}:{{otlpHttpsPort}}", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://{{hostName}}:{{resourceHttpsPort}}" + } + }, + "http": { + "applicationUrl": "http://{{hostName}}:{{httpPort}}", + "environmentVariables": { + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://{{hostName}}:{{otlpHttpPort}}", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://{{hostName}}:{{resourceHttpPort}}", + "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true" + } + } + } +} diff --git a/src/Aspire.Cli/Templating/Templates/py-starter/eslint.config.mjs b/src/Aspire.Cli/Templating/Templates/py-starter/eslint.config.mjs new file mode 100644 index 00000000000..e7e33edb3fd --- /dev/null +++ b/src/Aspire.Cli/Templating/Templates/py-starter/eslint.config.mjs @@ -0,0 +1,17 @@ +// @ts-check + +import { defineConfig } from 'eslint/config'; +import tseslint from 'typescript-eslint'; + +export default defineConfig({ + files: ['apphost.ts'], + extends: [tseslint.configs.base], + languageOptions: { + parserOptions: { + projectService: true, + }, + }, + rules: { + '@typescript-eslint/no-floating-promises': ['error', { checkThenables: true }], + }, +}); diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/.dockerignore b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/.dockerignore similarity index 100% rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/.dockerignore rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/.dockerignore diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/.gitignore b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/.gitignore similarity index 100% rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/.gitignore rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/.gitignore diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/eslint.config.js b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/eslint.config.js similarity index 100% rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/eslint.config.js rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/eslint.config.js diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/index.html b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/index.html similarity index 100% rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/index.html rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/index.html diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/package-lock.json b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/package-lock.json similarity index 100% rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/package-lock.json rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/package-lock.json diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/package.json b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/package.json similarity index 100% rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/package.json rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/package.json diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/public/Aspire.png b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/public/Aspire.png similarity index 100% rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/public/Aspire.png rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/public/Aspire.png diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/public/github.svg b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/public/github.svg similarity index 100% rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/public/github.svg rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/public/github.svg diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/src/App.css b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/src/App.css similarity index 100% rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/src/App.css rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/src/App.css diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/src/App.tsx b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/src/App.tsx similarity index 100% rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/src/App.tsx rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/src/App.tsx diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/src/index.css b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/src/index.css similarity index 100% rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/src/index.css rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/src/index.css diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/src/main.tsx b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/src/main.tsx similarity index 100% rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/src/main.tsx rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/src/main.tsx diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/src/vite-env.d.ts b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/src/vite-env.d.ts similarity index 100% rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/src/vite-env.d.ts rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/src/vite-env.d.ts diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/tsconfig.app.json b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/tsconfig.app.json similarity index 100% rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/tsconfig.app.json rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/tsconfig.app.json diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/tsconfig.json b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/tsconfig.json similarity index 100% rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/tsconfig.json rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/tsconfig.json diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/tsconfig.node.json b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/tsconfig.node.json similarity index 100% rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/tsconfig.node.json rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/tsconfig.node.json diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/vite.config.ts b/src/Aspire.Cli/Templating/Templates/py-starter/frontend/vite.config.ts similarity index 100% rename from src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/vite.config.ts rename to src/Aspire.Cli/Templating/Templates/py-starter/frontend/vite.config.ts diff --git a/src/Aspire.Cli/Templating/Templates/py-starter/package-lock.json b/src/Aspire.Cli/Templating/Templates/py-starter/package-lock.json new file mode 100644 index 00000000000..c750c10ace7 --- /dev/null +++ b/src/Aspire.Cli/Templating/Templates/py-starter/package-lock.json @@ -0,0 +1,2042 @@ +{ + "name": "{{projectNameLower}}", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "{{projectNameLower}}", + "dependencies": { + "vscode-jsonrpc": "^8.2.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "eslint": "^10.0.3", + "nodemon": "^3.1.14", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.57.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "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", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "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", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.3", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", + "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.3", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", + "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", + "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "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.3", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", + "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", + "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "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", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "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", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", + "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/type-utils": "8.57.1", + "@typescript-eslint/utils": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.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.57.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz", + "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", + "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.1", + "@typescript-eslint/types": "^8.57.1", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", + "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1" + }, + "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.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", + "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz", + "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/utils": "8.57.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.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.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", + "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", + "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.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", + "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.1", + "@typescript-eslint/tsconfig-utils": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.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.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz", + "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1" + }, + "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.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", + "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.1", + "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.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "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", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "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", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "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", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "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", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "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", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.3.tgz", + "integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.3", + "@eslint/config-helpers": "^0.5.2", + "@eslint/core": "^1.1.1", + "@eslint/plugin-kit": "^0.6.1", + "@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.1.1", + "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.4", + "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", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "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", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "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", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "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", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "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", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "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", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "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.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "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", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "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", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "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", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "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", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "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.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.1.tgz", + "integrity": "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.1", + "@typescript-eslint/parser": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/utils": "8.57.1" + }, + "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.0.0" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz", + "integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "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", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/src/Aspire.Cli/Templating/Templates/py-starter/package.json b/src/Aspire.Cli/Templating/Templates/py-starter/package.json new file mode 100644 index 00000000000..7878cd35dbe --- /dev/null +++ b/src/Aspire.Cli/Templating/Templates/py-starter/package.json @@ -0,0 +1,27 @@ +{ + "name": "{{projectNameLower}}", + "private": true, + "type": "module", + "scripts": { + "lint": "eslint apphost.ts", + "predev": "npm run lint", + "dev": "aspire run", + "prebuild": "npm run lint", + "build": "tsc", + "watch": "tsc --watch" + }, + "dependencies": { + "vscode-jsonrpc": "^8.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "eslint": "^10.0.3", + "nodemon": "^3.1.14", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.57.1" + } +} diff --git a/src/Aspire.Cli/Templating/Templates/py-starter/tsconfig.json b/src/Aspire.Cli/Templating/Templates/py-starter/tsconfig.json new file mode 100644 index 00000000000..1e44664428b --- /dev/null +++ b/src/Aspire.Cli/Templating/Templates/py-starter/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "bundler", + "strict": true + } +} diff --git a/src/Aspire.ProjectTemplates/Aspire.ProjectTemplates.csproj b/src/Aspire.ProjectTemplates/Aspire.ProjectTemplates.csproj index edcfe1a96af..ffdcba714f2 100644 --- a/src/Aspire.ProjectTemplates/Aspire.ProjectTemplates.csproj +++ b/src/Aspire.ProjectTemplates/Aspire.ProjectTemplates.csproj @@ -45,16 +45,10 @@ $(IntermediateOutputPath)content\templates\aspire-apphost-singlefile\%(RecursiveDir)%(Filename)%(Extension) - - - content/templates/aspire-py-starter/%(RecursiveDir) - $(IntermediateOutputPath)content\templates\aspire-py-starter\%(RecursiveDir)%(Filename)%(Extension) - - + Exclude="$(MSBuildThisFileDirectory)templates\**\bin\**;$(MSBuildThisFileDirectory)templates\**\obj\**;$(MSBuildThisFileDirectory)templates\**\*.csproj;$(MSBuildThisFileDirectory)templates\aspire-apphost-singlefile\**\apphost.cs"> content/templates/%(RecursiveDir) diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/dotnetcli.host.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/dotnetcli.host.json deleted file mode 100644 index 69f2bcb8b28..00000000000 --- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/dotnetcli.host.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/dotnetcli.host", - "symbolInfo": { - "NoHttps": { - "longName": "no-https", - "shortName": "" - }, - "UseRedisCache": { - "longName": "use-redis-cache", - "shortName": "" - }, - "LocalhostTld": { - "longName": "localhost-tld", - "shortName": "" - } - }, - "usageExamples": [] -} diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/ide.host.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/ide.host.json deleted file mode 100644 index d19d3fe8687..00000000000 --- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/ide.host.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/ide.host", - "unsupportedHosts": [ - { - "id": "vs" - } - ], - "symbolInfo": [ - { - "id": "UseRedisCache", - "isVisible": true, - "persistenceScope": "templateGroup" - }, - { - "id": "LocalhostTld", - "isVisible": true, - "persistenceScope": "templateGroup" - } - ] -} diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.cs.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.cs.json deleted file mode 100644 index 43a941d3199..00000000000 --- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.cs.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "author": "Microsoft", - "name": "Úvodní aplikace Aspire (FastAPI/React)", - "description": "Šablona projektu pro vytvoření aplikace Aspire s front-endem Reactu a službou back-endového rozhraní API Pythonu s volitelným ukládáním do mezipaměti Redis", - "symbols/UseRedisCache/displayName": "Po_užít Redis pro ukládání do mezipaměti (vyžaduje Docker)", - "symbols/UseRedisCache/description": "Nakonfiguruje, jestli se má aplikace nastavit tak, aby pro ukládání do mezipaměti používala Redis. K místnímu spouštění se vyžaduje podporovaný modul runtime kontejneru. Další podrobnosti najdete na https://aka.ms/aspire/containers.", - "symbols/appHostHttpPort/description": "Číslo portu, který se má použít pro koncový bod HTTP v launchSettings.json projektu AppHost.", - "symbols/appHostOtlpHttpPort/description": "Číslo portu, který se má použít pro koncový bod HTTP OTLP v launchSettings.json projektu AppHost.", - "symbols/appHostResourceHttpPort/description": "Číslo portu, který se má použít pro koncový bod HTTP služby prostředků v launchSettings.json projektu AppHost.", - "symbols/appHostHttpsPort/description": "Číslo portu, který se má použít pro koncový bod HTTPS v launchSettings.json projektu AppHost. Tato možnost se dá použít jenom v případě, že se nepoužívá parametr no-https.", - "symbols/appHostOtlpHttpsPort/description": "Číslo portu, který se má použít pro koncový bod HTTPS OTLP v launchSettings.json projektu AppHost.", - "symbols/appHostResourceHttpsPort/description": "Číslo portu, který se má použít pro koncový bod HTTPS služby prostředků v launchSettings.json projektu AppHost.", - "symbols/NoHttps/description": "Určuje, jestli se má vypnout protokol HTTPS.", - "symbols/LocalhostTld/displayName": "Použití TLD .dev.localhost v adrese URL aplikace", - "symbols/LocalhostTld/description": "Určuje, jestli se má název projektu zkombinovat s TLD .dev.localhost v adrese URL aplikace pro místní vývoj, například https://myapp.dev.localhost:12345. Podporováno v .NET 10 a novějších verzích." -} \ No newline at end of file diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.de.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.de.json deleted file mode 100644 index 9b84c7a820a..00000000000 --- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.de.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "author": "Microsoft", - "name": "Aspire-Starter-App (FastAPI/React)", - "description": "Eine Projektvorlage zum Erstellen einer Aspire-App mit einem React-Front-End- und einem Python-Back-End-API-Dienst, optional mit Redis-Zwischenspeicherung.", - "symbols/UseRedisCache/displayName": "_Use Redis für die Zwischenspeicherung (erfordert eine unterstützte Container-Runtime)", - "symbols/UseRedisCache/description": "Legt fest, ob die Anwendung für die Verwendung von Redis zum Zwischenspeichern eingerichtet werden soll. Erfordert eine unterstützte Containerruntime für die lokale Ausführung. Weitere Informationen finden Sie unter https://aka.ms/aspire/containers.", - "symbols/appHostHttpPort/description": "Portnummer, die für den HTTP-Endpunkt in launchSettings.json des AppHost-Projekts verwendet werden soll.", - "symbols/appHostOtlpHttpPort/description": "Portnummer, die für den OTLP-HTTP-Endpunkt in launchSettings.json des AppHost-Projekts verwendet werden soll.", - "symbols/appHostResourceHttpPort/description": "Portnummer, die für den HTTP-Endpunkt des Ressourcendiensts in launchSettings.json des AppHost-Projekts verwendet werden soll.", - "symbols/appHostHttpsPort/description": "Portnummer, die für den HTTPS-Endpunkt in launchSettings.json des AppHost-Projekts verwendet werden soll. Diese Option ist nur anwendbar, wenn der Parameter no-https nicht verwendet wird.", - "symbols/appHostOtlpHttpsPort/description": "Portnummer, die für den OTLP-HTTPS-Endpunkt in launchSettings.json des AppHost-Projekts verwendet werden soll.", - "symbols/appHostResourceHttpsPort/description": "Portnummer, die für den HTTPS-Endpunkt des Ressourcendiensts in launchSettings.json des AppHost-Projekts verwendet werden soll.", - "symbols/NoHttps/description": "Ob HTTPS deaktiviert werden soll.", - "symbols/LocalhostTld/displayName": "Verwenden der .dev.localhost-TLD in der Anwendungs-URL", - "symbols/LocalhostTld/description": "Gibt an, ob der Projektname mit der .dev.localhost-TLD in der Anwendungs-URL für die lokale Entwicklung kombiniert werden soll, z. B. https://myapp.dev.localhost:12345. Unterstützt unter .NET 10 und höher." -} \ No newline at end of file diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.en.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.en.json deleted file mode 100644 index cbdb3adc4ae..00000000000 --- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.en.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "author": "Microsoft", - "name": "Aspire Starter App (FastAPI/React)", - "description": "A project template for creating an Aspire app with a React frontend and a Python backend API service, with optional Redis caching.", - "symbols/UseRedisCache/displayName": "_Use Redis for caching (requires a supported container runtime)", - "symbols/UseRedisCache/description": "Configures whether to setup the application to use Redis for caching. Requires a supported container runtime to run locally, see https://aka.ms/aspire/containers for more details.", - "symbols/appHostHttpPort/description": "Port number to use for the HTTP endpoint in launchSettings.json of the AppHost project.", - "symbols/appHostOtlpHttpPort/description": "Port number to use for the OTLP HTTP endpoint in launchSettings.json of the AppHost project.", - "symbols/appHostResourceHttpPort/description": "Port number to use for the resource service HTTP endpoint in launchSettings.json of the AppHost project.", - "symbols/appHostHttpsPort/description": "Port number to use for the HTTPS endpoint in launchSettings.json of the AppHost project. This option is only applicable when the parameter no-https is not used.", - "symbols/appHostOtlpHttpsPort/description": "Port number to use for the OTLP HTTPS endpoint in launchSettings.json of the AppHost project.", - "symbols/appHostResourceHttpsPort/description": "Port number to use for the resource service HTTPS endpoint in launchSettings.json of the AppHost project.", - "symbols/NoHttps/description": "Whether to turn off HTTPS.", - "symbols/LocalhostTld/displayName": "Use the .dev.localhost TLD in the application URL", - "symbols/LocalhostTld/description": "Whether to combine the project name with the .dev.localhost TLD in the application URL for local development, e.g. https://myapp.dev.localhost:12345. Supported on .NET 10 and later." -} \ No newline at end of file diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.es.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.es.json deleted file mode 100644 index c918c1744da..00000000000 --- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.es.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "author": "Microsoft", - "name": "Aplicación starter de Starter (FastAPI/React)", - "description": "Plantilla de proyecto para crear una aplicación Desi con un front-end de React y un servicio de API de back-end de Python, con almacenamiento en caché de Redis opcional.", - "symbols/UseRedisCache/displayName": "_Use Redis para el almacenamiento en caché (requiere un runtime de contenedor compatible)", - "symbols/UseRedisCache/description": "Configure si se va a configurar la aplicación para que use Redis para el almacenamiento en caché. Requiere un entorno de ejecución de contenedor compatible para ejecutarse localmente. Consulte https://aka.ms/aspire/containers para obtener más detalles.", - "symbols/appHostHttpPort/description": "Número de puerto que se va a usar para el punto de conexión HTTP en launchSettings.json del proyecto AppHost.", - "symbols/appHostOtlpHttpPort/description": "Número de puerto que se va a usar para el punto de conexión HTTP de OTLP en launchSettings.json del proyecto AppHost.", - "symbols/appHostResourceHttpPort/description": "Número de puerto que se va a usar para el punto de conexión HTTP del servicio de recursos en launchSettings.json del proyecto AppHost.", - "symbols/appHostHttpsPort/description": "Número de puerto que se va a usar para el punto de conexión HTTPS en launchSettings.json del proyecto AppHost. Esta opción solo es aplicable cuando no se usa el parámetro no-https.", - "symbols/appHostOtlpHttpsPort/description": "Número de puerto que se va a usar para el punto de conexión HTTPS de OTLP en launchSettings.json del proyecto AppHost.", - "symbols/appHostResourceHttpsPort/description": "Número de puerto a usar para el punto de conexión HTTPS del servicio de recursos en launchSettings.json del proyecto AppHost.", - "symbols/NoHttps/description": "Si se va a desactivar HTTPS.", - "symbols/LocalhostTld/displayName": "Usar el TLD .dev.localhost en la dirección URL de la aplicación", - "symbols/LocalhostTld/description": "Si se debe combinar el nombre del proyecto con el TLD .dev.localhost en la URL de la aplicación para el desarrollo local, por ejemplo, https://myapp.dev.localhost:12345. Compatible con .NET 10 y versiones posteriores." -} \ No newline at end of file diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.fr.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.fr.json deleted file mode 100644 index eb1bbcf8f3e..00000000000 --- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.fr.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "author": "Microsoft", - "name": "Application de démarrage Aspire (FastAPI/React)", - "description": "Modèle de projet pour créer une application Aspire avec un front-end React et un service API back-end en Python, avec une mise en cache Redis optionnelle.", - "symbols/UseRedisCache/displayName": "_Use Redis pour la mise en cache (nécessite un runtime de conteneur pris en charge)", - "symbols/UseRedisCache/description": "Permet la configuration s’il faut configurer l’application afin qu’elle utilise Redis pour la mise en cache. Nécessite un runtime du conteneur pris en charge pour fonctionner localement, voir https://aka.ms/aspire/containers pour obtenir plus d’informations.", - "symbols/appHostHttpPort/description": "Numéro de port à utiliser pour le point de terminaison HTTP dans launchSettings.json du projet AppHost.", - "symbols/appHostOtlpHttpPort/description": "Numéro de port à utiliser pour le point de terminaison HTTP OTLP dans launchSettings.json du projet AppHost.", - "symbols/appHostResourceHttpPort/description": "Numéro de port à utiliser pour le point de terminaison HTTP du service de ressources dans launchSettings.json du projet AppHost.", - "symbols/appHostHttpsPort/description": "Numéro de port à utiliser pour le point de terminaison HTTPS dans launchSettings.json du projet AppHost. Cette option n'est applicable que lorsque le paramètre no-https n'est pas utilisé.", - "symbols/appHostOtlpHttpsPort/description": "Numéro de port à utiliser pour le point de terminaison HTTPS OTLP dans launchSettings.json du projet AppHost.", - "symbols/appHostResourceHttpsPort/description": "Numéro de port à utiliser pour le point de terminaison HTTPS du service de ressources dans launchSettings.json du projet AppHost.", - "symbols/NoHttps/description": "Indique s’il faut désactiver HTTPS.", - "symbols/LocalhostTld/displayName": "Utilisez le TLD .dev.localhost dans l’URL de l’application", - "symbols/LocalhostTld/description": "Indique s’il faut combiner le nom du projet avec le fichier TLD .dev.localhost dans l’URL de l’application pour le développement local, par exemple https://myapp.dev.localhost:12345. Pris en charge sur .NET 10 et versions ultérieures." -} \ No newline at end of file diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.it.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.it.json deleted file mode 100644 index bf1d2ad2ecd..00000000000 --- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.it.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "author": "Microsoft", - "name": "App Starter Aspire (FastAPI/React)", - "description": "Modello di progetto per la creazione di un'app Aspire con front-end React e servizio API back-end Python, con cache Redis opzionale.", - "symbols/UseRedisCache/displayName": "_Usare Redis per la memorizzazione nella cache (richiede un runtime del contenitore supportato)", - "symbols/UseRedisCache/description": "Configura se impostare l'applicazione per l'utilizzo di Redis per la memorizzazione nella cache. Richiede l'esecuzione locale di un runtime del contenitore supportato. Per altri dettagli, vedere https://aka.ms/aspire/containers.", - "symbols/appHostHttpPort/description": "Numero di porta da usare per l'endpoint HTTP in launchSettings.json. del progetto AppHost.", - "symbols/appHostOtlpHttpPort/description": "Numero di porta da usare per l'endpoint OTLP HTTP in launchSettings.json. del progetto AppHost.", - "symbols/appHostResourceHttpPort/description": "Numero di porta da usare per l'endpoint HTTP del servizio risorse in launchSettings.json del progetto AppHost.", - "symbols/appHostHttpsPort/description": "Numero di porta da usare per l'endpoint HTTPS in launchSettings.json. del progetto AppHost. Questa opzione è applicabile solo quando il parametro no-https non viene usato.", - "symbols/appHostOtlpHttpsPort/description": "Numero di porta da usare per l'endpoint OTLP HTTPS in launchSettings.json. del progetto AppHost.", - "symbols/appHostResourceHttpsPort/description": "Numero di porta da usare per l'endpoint HTTPS del servizio risorse in launchSettings.json del progetto AppHost.", - "symbols/NoHttps/description": "Indica se disattivare HTTPS.", - "symbols/LocalhostTld/displayName": "Usa il TLD .dev.localhost nell'URL dell'applicazione", - "symbols/LocalhostTld/description": "Indica se combinare il nome del progetto con il dominio di primo livello .dev.localhost nell'URL dell'applicazione per lo sviluppo in locale, ad esempio https://myapp.dev.localhost:12345. Supportato in .NET 10 e versioni successive." -} \ No newline at end of file diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.ja.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.ja.json deleted file mode 100644 index 6c0ced89797..00000000000 --- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.ja.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "author": "Microsoft", - "name": "Aspire スターター アプリ (FastAPI/React)", - "description": "React フロントエンドと Python バックエンド API サービスを使用して Aspire アプリを作成するためのプロジェクト テンプレート。必要に応じて、Redis キャッシュを使用します。", - "symbols/UseRedisCache/displayName": "キャッシュ用に Redis を使用する (サポートされているコンテナー ランタイムが必要) (_U)", - "symbols/UseRedisCache/description": "Redis をキャッシュに使用するようにアプリケーションを設定するかどうかを構成します。ローカルで実行するには、サポートされているコンテナー ランタイムが必要です。詳細については、https://aka.ms/aspire/containers を参照してください。", - "symbols/appHostHttpPort/description": "AppHost プロジェクトの launchSettings.json の HTTP エンドポイントに使用するポート番号。", - "symbols/appHostOtlpHttpPort/description": "AppHost プロジェクトの launchSettings.json で OTLP HTTP エンドポイントに使用するポート番号。", - "symbols/appHostResourceHttpPort/description": "AppHost プロジェクトの launchSettings.json のリソース サービス HTTP エンドポイントに使用するポート番号。", - "symbols/appHostHttpsPort/description": "AppHost プロジェクトの launchSettings.json の HTTPS エンドポイントに使用するポート番号。このオプションは、パラメーター no-https を使用しない場合にのみ適用されます。", - "symbols/appHostOtlpHttpsPort/description": "AppHost プロジェクトの launchSettings.json で OTLP HTTPS エンドポイントに使用するポート番号。", - "symbols/appHostResourceHttpsPort/description": "AppHost プロジェクトの launchSettings.json のリソース サービス HTTPS エンドポイントに使用するポート番号。", - "symbols/NoHttps/description": "HTTPS をオフにするかどうか。", - "symbols/LocalhostTld/displayName": "アプリケーション URL で .dev.localhost TLD を使用する", - "symbols/LocalhostTld/description": "ローカル開発用のアプリケーション URL 内で、プロジェクト名を .dev.localhost TLD と組み合わせるかどうか (例: https://myapp.dev.localhost:12345)。.NET 10 以降でサポートされています。" -} \ No newline at end of file diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.ko.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.ko.json deleted file mode 100644 index 67d44613363..00000000000 --- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.ko.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "author": "Microsoft", - "name": "Aspire 시작 앱(FastAPI/React)", - "description": "선택적 Redis 캐싱을 사용하여 React 프런트 엔드와 Python 백 엔드 API 서비스로 Aspire 앱을 만들기 위한 프로젝트 템플릿입니다.", - "symbols/UseRedisCache/displayName": "캐싱용 _Use Redis(지원되는 컨테이너 런타임 필요)", - "symbols/UseRedisCache/description": "캐싱에 Redis를 사용하도록 애플리케이션을 설정할지 여부를 구성합니다. 로컬로 실행하려면 지원되는 컨테이너 런타임이 필요합니다. 자세한 내용은 https://aka.ms/aspire/containers를 참조하세요.", - "symbols/appHostHttpPort/description": "AppHost 프로젝트의 launchSettings.json HTTP 엔드포인트에 사용할 포트 번호입니다.", - "symbols/appHostOtlpHttpPort/description": "AppHost 프로젝트의 launchSettings.json OTLP HTTP 엔드포인트에 사용할 포트 번호입니다.", - "symbols/appHostResourceHttpPort/description": "AppHost 프로젝트의 launchSettings.json 리소스 서비스 HTTP 엔드포인트에 사용할 포트 번호입니다.", - "symbols/appHostHttpsPort/description": "AppHost 프로젝트의 launchSettings.json HTTPS 엔드포인트에 사용할 포트 번호입니다. 이 옵션은 no-https 매개 변수가 사용되지 않는 경우에만 적용됩니다.", - "symbols/appHostOtlpHttpsPort/description": "AppHost 프로젝트의 launchSettings.json OTLP HTTPS 엔드포인트에 사용할 포트 번호입니다.", - "symbols/appHostResourceHttpsPort/description": "AppHost 프로젝트의 launchSettings.json 리소스 서비스 HTTPS 엔드포인트에 사용할 포트 번호입니다.", - "symbols/NoHttps/description": "HTTPS를 끌지 여부입니다.", - "symbols/LocalhostTld/displayName": "애플리케이션 URL에서 .dev.localhost TLD를 사용하세요.", - "symbols/LocalhostTld/description": "로컬 개발을 위한 애플리케이션 URL에서 프로젝트 이름과 .dev.localhost TLD를 결합할지 여부(예: https://myapp.dev.localhost:12345)입니다. .NET 10 이상에서 지원됩니다." -} \ No newline at end of file diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.pl.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.pl.json deleted file mode 100644 index 47245ae7a9f..00000000000 --- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.pl.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "author": "Microsoft", - "name": "Aplikacja startowa Aspire (FastAPI/React)", - "description": "Szablon projektu do tworzenia aplikacji Aspire z frontonem React i wewnętrzną bazą danych interfejsu API w Pythonie, z opcjonalnym buforowaniem Redis.", - "symbols/UseRedisCache/displayName": "_Skorzystaj z magazynu danych Redis na potrzeby buforowania (wymaga obsługiwanego środowiska uruchomieniowego kontenera)", - "symbols/UseRedisCache/description": "Określa, czy konfigurować aplikację do korzystania z magazynu danych Redis na potrzeby buforowania. Wymaga obsługiwanego środowiska uruchomieniowego kontenera na potrzeby uruchomienia lokalnego. Aby uzyskać więcej informacji, zobacz https://aka.ms/aspire/containers.", - "symbols/appHostHttpPort/description": "Numer portu do użycia dla punktu końcowego HTTP w pliku launchSettings.json projektu AppHost.", - "symbols/appHostOtlpHttpPort/description": "Numer portu do użycia dla punktu końcowego HTTP OTLP w pliku launchSettings.json projektu AppHost.", - "symbols/appHostResourceHttpPort/description": "Numer portu do użycia dla punktu końcowego HTTP usługi zasobów w pliku launchSettings.json projektu AppHost.", - "symbols/appHostHttpsPort/description": "Numer portu do użycia dla punktu końcowego HTTPS w pliku launchSettings.json projektu AppHost. Ta opcja ma zastosowanie tylko wtedy, gdy parametr no-https nie jest używany.", - "symbols/appHostOtlpHttpsPort/description": "Numer portu do użycia dla punktu końcowego HTTPS OTLP w pliku launchSettings.json projektu AppHost.", - "symbols/appHostResourceHttpsPort/description": "Numer portu do użycia dla punktu końcowego HTTPS usługi zasobów w pliku launchSettings.json projektu AppHost.", - "symbols/NoHttps/description": "Określa, czy wyłączyć protokół HTTPS.", - "symbols/LocalhostTld/displayName": "Użyj pliku .dev.localhost TLD w adresie URL aplikacji", - "symbols/LocalhostTld/description": "Określa, czy połączyć nazwę projektu z TLD .dev.localhost w adresie URL aplikacji na potrzeby lokalnego programowania, np. https://myapp.dev.localhost:12345. Obsługiwane na platformie .NET 10 lub nowszej." -} \ No newline at end of file diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.pt-BR.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.pt-BR.json deleted file mode 100644 index d86ab883ff3..00000000000 --- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.pt-BR.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "author": "Microsoft", - "name": "App de Inicialização Aspire (FastAPI/React)", - "description": "Um modelo de projeto para criar um aplicativo Aspire com um front-end do React e um serviço de API de back-end do Python, com cache Redis opcional.", - "symbols/UseRedisCache/displayName": "_Usar o Redis para cache (requer um tempo de execução de contêiner compatível)", - "symbols/UseRedisCache/description": "Configura se o aplicativo deve usar Redis para cache. Requer um runtime de contêiner com suporte para execução local; confira https://aka.ms/aspire/containers para obter mais detalhes.", - "symbols/appHostHttpPort/description": "Número da porta a ser usado para o ponto de extremidade HTTP launchSettings.json do projeto AppHost.", - "symbols/appHostOtlpHttpPort/description": "Número da porta a ser usado para o ponto de extremidade HTTP OTLP launchSettings.json do projeto AppHost.", - "symbols/appHostResourceHttpPort/description": "Número da porta a ser usado para o ponto de extremidade HTTP do serviço de recurso launchSettings.json do projeto AppHost.", - "symbols/appHostHttpsPort/description": "Número da porta a ser usado para o ponto de extremidade HTTPS launchSettings.json do projeto AppHost. Essa opção só é aplicável quando o parâmetro no-https não é usado.", - "symbols/appHostOtlpHttpsPort/description": "Número da porta a ser usado para o ponto de extremidade HTTPS OTLP launchSettings.json do projeto AppHost.", - "symbols/appHostResourceHttpsPort/description": "Número da porta a ser usado para o ponto de extremidade HTTPS do serviço de recurso launchSettings.json do projeto AppHost.", - "symbols/NoHttps/description": "Se o HTTPS deve ser desativado.", - "symbols/LocalhostTld/displayName": "Usar o TLD .dev.localhost na URL do aplicativo", - "symbols/LocalhostTld/description": "Se quiser combinar o nome do projeto com o TLD .dev.localhost na URL do aplicativo para desenvolvimento local, por exemplo, https://myapp.dev.localhost:12345. Com suporte no .NET 10 e versões posteriores." -} \ No newline at end of file diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.ru.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.ru.json deleted file mode 100644 index 7fe0cfa0546..00000000000 --- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.ru.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "author": "Майкрософт", - "name": "Начальное приложение Aspire (FastAPI/React)", - "description": "Шаблон проекта для создания приложения Aspire с интерфейсной частью React и службой API серверной части на Python с необязательным кэшированием Redis.", - "symbols/UseRedisCache/displayName": "_Использовать Redis для кэширования (требуется поддерживаемая среда выполнения контейнера)", - "symbols/UseRedisCache/description": "Определяет, указывать ли приложению, что для кэширования следует использовать Redis. Для локального запуска требуется поддерживаемая среда выполнения контейнера. Дополнительные сведения см. на странице https://aka.ms/aspire/containers.", - "symbols/appHostHttpPort/description": "Номер порта, который будет использоваться для конечной точки HTTP в файле launchSettings.json проекта AppHost.", - "symbols/appHostOtlpHttpPort/description": "Номер порта, который будет использоваться для конечной точки HTTP OTLP в файле launchSettings.json проекта AppHost.", - "symbols/appHostResourceHttpPort/description": "Номер порта, который будет использоваться для конечной точки HTTP службы ресурсов в файле launchSettings.json проекта AppHost.", - "symbols/appHostHttpsPort/description": "Номер порта, который будет использоваться для конечной точки HTTPS в файле launchSettings.json проекта AppHost. Этот параметр применим только в том случае, если NO-HTTP не используется.", - "symbols/appHostOtlpHttpsPort/description": "Номер порта, который будет использоваться для конечной точки OTLP HTTPS в файле launchSettings.json проекта AppHost.", - "symbols/appHostResourceHttpsPort/description": "Номер порта, который будет использоваться для конечной точки HTTPS службы ресурсов в файле launchSettings.json проекта AppHost.", - "symbols/NoHttps/description": "Следует ли отключить HTTPS.", - "symbols/LocalhostTld/displayName": "Используйте домен верхнего уровня .dev.localhost в URL-адресе приложения", - "symbols/LocalhostTld/description": "Следует ли объединять имя проекта с доменом верхнего уровня .dev.localhost в URL-адресе приложения для локальной разработки, например https://myapp.dev.localhost:12345. Поддерживается в .NET 10 и более поздних версиях." -} \ No newline at end of file diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.tr.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.tr.json deleted file mode 100644 index c93f2ed0898..00000000000 --- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.tr.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "author": "Microsoft", - "name": "Aspire Başlangıç Uygulaması (FastAPI/React)", - "description": "İsteğe bağlı Redis önbelleğiyle bir React ön uç ve Python arka uç API hizmetini içeren bir Aspire uygulaması oluşturmak için proje şablonu.", - "symbols/UseRedisCache/displayName": "Önbelleğe alma için Redis’i k_ullan (desteklenen bir kapsayıcı çalışma zamanı gerektirir)", - "symbols/UseRedisCache/description": "Uygulamanın önbelleğe alma için Redis’i kullanmak üzere ayarlanıp ayarlanmayacağını yapılandırır. Yerel olarak çalıştırmak için desteklenen bir kapsayıcı çalışma zamanı gerektirir. Daha fazla ayrıntı için https://aka.ms/aspire/containers sayfasına bakın.", - "symbols/appHostHttpPort/description": "AppHost projesinin HTTP uç noktası launchSettings.json bağlantı noktası numarası.", - "symbols/appHostOtlpHttpPort/description": "AppHost projesinin OTLP HTTP uç noktası launchSettings.json bağlantı noktası numarası.", - "symbols/appHostResourceHttpPort/description": "AppHost projesinin kaynak hizmeti HTTP uç noktası launchSettings.json bağlantı noktası numarası.", - "symbols/appHostHttpsPort/description": "AppHost projesinin HTTPS uç noktası launchSettings.json bağlantı noktası numarası. Bu seçenek yalnızca no-https parametresi kullanılmadığında uygulanabilir.", - "symbols/appHostOtlpHttpsPort/description": "AppHost projesinin OTLP HTTPS uç noktası launchSettings.json bağlantı noktası numarası.", - "symbols/appHostResourceHttpsPort/description": "AppHost projesinin kaynak hizmeti HTTPS uç noktası launchSettings.json bağlantı noktası numarası.", - "symbols/NoHttps/description": "HTTPS'nin kapatılıp kapatılmayacağı.", - "symbols/LocalhostTld/displayName": "Uygulama URL'sinde .dev.localhost TLD'sini kullanın.", - "symbols/LocalhostTld/description": "Yerel geliştirme için uygulama URL'sinde proje adının .dev.localhost TLD ile birleştirilip birleştirilmeyeceği, örneğin https://myapp.dev.localhost:12345. .NET 10 ve sonraki sürümlerde desteklenir." -} \ No newline at end of file diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.zh-Hans.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.zh-Hans.json deleted file mode 100644 index 154115b92ad..00000000000 --- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.zh-Hans.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "author": "Microsoft", - "name": "Aspire 入门应用(FastAPI/React)", - "description": "用于使用 React 前端和 Python 后端 API 服务创建 Aspire 应用的项目模板,可选择使用 Redis 缓存。", - "symbols/UseRedisCache/displayName": "使用 Redis 进行缓存(需要受支持的容器运行时)(_U)", - "symbols/UseRedisCache/description": "配置是否将应用程序设置为使用 Redis 进行缓存。需要支持的容器运行时才能在本地运行,有关详细信息,请访问 https://aka.ms/aspire/containers for more details。", - "symbols/appHostHttpPort/description": "该端口号将用于 AppHost 项目的 launchSettings.json 中的 HTTP 终结点。", - "symbols/appHostOtlpHttpPort/description": "该端口号将用于 AppHost 项目的 launchSettings.json 中的 OTLP HTTP 终结点。", - "symbols/appHostResourceHttpPort/description": "该端口号将用于 AppHost 项目的 launchSettings.json 中的资源服务 HTTP 终结点。", - "symbols/appHostHttpsPort/description": "该端口号将用于 AppHost 项目的 launchSettings.json 中的 HTTPS 终结点。仅当不使用参数 no-https 时,此选项才适用。", - "symbols/appHostOtlpHttpsPort/description": "该端口号将用于 AppHost 项目的 launchSettings.json 中的 OTLP HTTPS 终结点。", - "symbols/appHostResourceHttpsPort/description": "该端口号将用于 AppHost 项目的 launchSettings.json 中的资源服务 HTTPS 终结点。", - "symbols/NoHttps/description": "是否禁用 HTTPS。", - "symbols/LocalhostTld/displayName": "在应用程序 URL 中使用 .dev.localhost TLD", - "symbols/LocalhostTld/description": "是否在应用程序 URL 中将项目名称与 .dev.localhost TLD 合并以用于本地开发,例如 https://myapp.dev.localhost:12345。在 .NET 10 及更高版本中受支持。" -} \ No newline at end of file diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.zh-Hant.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.zh-Hant.json deleted file mode 100644 index 053127d5ae8..00000000000 --- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/localize/templatestrings.zh-Hant.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "author": "Microsoft", - "name": "Aspire 入門應用程式 (FastAPI/React)", - "description": "用於建立 Aspire 應用程式的專案範本,其中包含 React 前端與 Python 後端 API 服務,並可選用 Redis 快取功能。", - "symbols/UseRedisCache/displayName": "使用 Redis 進行快取 (需要支援的容器執行階段)(_U)", - "symbols/UseRedisCache/description": "設定是否要將應用程式設為使用 Redis 進行快取。需要支援的容器執行階段,才能在本機執行,如需詳細資料,請參閱 https://aka.ms/aspire/containers。", - "symbols/appHostHttpPort/description": "要用於 AppHost 專案 launchSettings.json 中 HTTP 端點的連接埠號碼。", - "symbols/appHostOtlpHttpPort/description": "要用於 AppHost 專案 launchSettings.json 中 OTLP HTTP 端點的連接埠號碼。", - "symbols/appHostResourceHttpPort/description": "要用於 AppHost 專案 launchSettings.json 中資源服務 HTTP 端點的連接埠號碼。", - "symbols/appHostHttpsPort/description": "要用於 AppHost 專案 launchSettings.json 中 HTTPS 端點的連接埠號碼。只有在未使用參數 no-https 時,才適用此選項。", - "symbols/appHostOtlpHttpsPort/description": "要用於 AppHost 專案 launchSettings.json 中 OTLP HTTPS 端點的連接埠號碼。", - "symbols/appHostResourceHttpsPort/description": "要用於 AppHost 專案 launchSettings.json 中資源服務 HTTPS 端點的連接埠號碼。", - "symbols/NoHttps/description": "是否要關閉 HTTPS。", - "symbols/LocalhostTld/displayName": "在應用程式 URL 中使用 .dev.localhost TLD", - "symbols/LocalhostTld/description": "是否要在用於本機開發的應用程式 URL 中將專案名稱結合 .dev.localhost TLD,例如 https://myapp.dev.localhost:12345。在 .NET 10 及更新版本上支援。" -} \ No newline at end of file diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/template.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/template.json deleted file mode 100644 index e6ae244d653..00000000000 --- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/.template.config/template.json +++ /dev/null @@ -1,245 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/template", - "author": "Microsoft", - "classifications": [ - "Common", - "Aspire", - "Cloud", - "Web", - "Web API", - "API", - "Service", - "Python", - "TypeScript", - "React" - ], - "name": "Aspire Starter App (FastAPI/React)", - "defaultName": "AspirePyApp", - "description": "A project template for creating an Aspire app with a React frontend and a Python backend API service, with optional Redis caching.", - "shortName": "aspire-py-starter", - "sourceName": "AspirePyStarter.1", - "preferNameDirectory": true, - "tags": { - "language": "Python", - "type": "project" - }, - "precedence": "9000", - "identity": "Aspire.Starter.TypeScript.Python.!!REPLACE_WITH_LATEST_MAJOR_MINOR_VERSION!!", - "thirdPartyNotices": "https://aka.ms/aspire/third-party-notices", - "groupIdentity": "Aspire.Starter.TypeScript.Python", - "guids": [ - "A1B2C3D4-E5F6-7890-ABCD-EF1234567890", - "B2C3D4E5-F678-90AB-CDEF-123456789012" - ], - "sources": [ - { - "source": "./", - "target": "./" - } - ], - "symbols": { - "UseRedisCache": { - "type": "parameter", - "datatype": "bool", - "defaultValue": "false", - "displayName": "_Use Redis for caching (requires a supported container runtime)", - "description": "Configures whether to setup the application to use Redis for caching. Requires a supported container runtime to run locally, see https://aka.ms/aspire/containers for more details." - }, - "appHostHttpPort": { - "type": "parameter", - "datatype": "integer", - "description": "Port number to use for the HTTP endpoint in launchSettings.json of the AppHost project." - }, - "appHostHttpPortGenerated": { - "type": "generated", - "generator": "port", - "parameters": { - "low": 15000, - "high": 15300 - } - }, - "appHostHttpPortReplacer": { - "type": "generated", - "generator": "coalesce", - "parameters": { - "sourceVariableName": "appHostHttpPort", - "fallbackVariableName": "appHostHttpPortGenerated" - }, - "replaces": "15000" - }, - "appHostOtlpHttpPort": { - "type": "parameter", - "datatype": "integer", - "description": "Port number to use for the OTLP HTTP endpoint in launchSettings.json of the AppHost project." - }, - "appHostOtlpHttpPortGenerated": { - "type": "generated", - "generator": "port", - "parameters": { - "low": 19000, - "high": 19300 - } - }, - "appHostOtlpHttpPortReplacer": { - "type": "generated", - "generator": "coalesce", - "parameters": { - "sourceVariableName": "appHostOtlpHttpPort", - "fallbackVariableName": "appHostOtlpHttpPortGenerated" - }, - "replaces": "19000" - }, - "appHostResourceHttpPort": { - "type": "parameter", - "datatype": "integer", - "description": "Port number to use for the resource service HTTP endpoint in launchSettings.json of the AppHost project." - }, - "appHostResourceHttpPortGenerated": { - "type": "generated", - "generator": "port", - "parameters": { - "low": 20000, - "high": 20300 - } - }, - "appHostResourceHttpPortReplacer": { - "type": "generated", - "generator": "coalesce", - "parameters": { - "sourceVariableName": "appHostResourceHttpPort", - "fallbackVariableName": "appHostResourceHttpPortGenerated" - }, - "replaces": "20000" - }, - "appHostHttpsPort": { - "type": "parameter", - "datatype": "integer", - "description": "Port number to use for the HTTPS endpoint in launchSettings.json of the AppHost project. This option is only applicable when the parameter no-https is not used." - }, - "appHostHttpsPortGenerated": { - "type": "generated", - "generator": "port", - "parameters": { - "low": 17000, - "high": 17300 - } - }, - "appHostHttpsPortReplacer": { - "type": "generated", - "generator": "coalesce", - "parameters": { - "sourceVariableName": "appHostHttpsPort", - "fallbackVariableName": "appHostHttpsPortGenerated" - }, - "replaces": "17000" - }, - "appHostOtlpHttpsPort": { - "type": "parameter", - "datatype": "integer", - "description": "Port number to use for the OTLP HTTPS endpoint in launchSettings.json of the AppHost project." - }, - "appHostOtlpHttpsPortGenerated": { - "type": "generated", - "generator": "port", - "parameters": { - "low": 21000, - "high": 21300 - } - }, - "appHostOtlpHttpsPortReplacer": { - "type": "generated", - "generator": "coalesce", - "parameters": { - "sourceVariableName": "appHostOtlpHttpsPort", - "fallbackVariableName": "appHostOtlpHttpsPortGenerated" - }, - "replaces": "21000" - }, - "appHostResourceHttpsPort": { - "type": "parameter", - "datatype": "integer", - "description": "Port number to use for the resource service HTTPS endpoint in launchSettings.json of the AppHost project." - }, - "appHostResourceHttpsPortGenerated": { - "type": "generated", - "generator": "port", - "parameters": { - "low": 22000, - "high": 22300 - } - }, - "appHostResourceHttpsPortReplacer": { - "type": "generated", - "generator": "coalesce", - "parameters": { - "sourceVariableName": "appHostResourceHttpsPort", - "fallbackVariableName": "appHostResourceHttpsPortGenerated" - }, - "replaces": "22000" - }, - "HasHttpsProfile": { - "type": "computed", - "value": "(!NoHttps)" - }, - "NoHttps": { - "type": "parameter", - "datatype": "bool", - "defaultValue": "false", - "description": "Whether to turn off HTTPS." - }, - "hostIdentifier": { - "type": "bind", - "binding": "HostIdentifier" - }, - "LocalhostTld": { - "type": "parameter", - "datatype": "bool", - "defaultValue": "false", - "displayName": "Use the .dev.localhost TLD in the application URL", - "description": "Whether to combine the project name with the .dev.localhost TLD in the application URL for local development, e.g. https://myapp.dev.localhost:12345. Supported on .NET 10 and later." - }, - "hostName": { - "type": "derived", - "valueSource": "name", - "valueTransform": "lowerCaseInvariantWithHyphens", - "replaces": "LocalhostTldHostNamePrefix" - } - }, - "forms": { - "lowerCaseInvariantWithHyphens": { - "identifier": "chain", - "steps": [ - "lowerCaseInvariant", - "replaceDnsInvalidCharsWithHyphens", - "replaceRepeatedHyphens", - "trimLeadingHyphens", - "trimTrailingHyphens" - ] - }, - "replaceDnsInvalidCharsWithHyphens": { - "identifier": "replace", - "pattern": "[^a-z0-9-]", - "replacement": "-" - }, - "replaceRepeatedHyphens": { - "identifier": "replace", - "pattern": "-{2,}", - "replacement": "-" - }, - "trimLeadingHyphens": { - "identifier": "replace", - "pattern": "^-+", - "replacement": "" - }, - "trimTrailingHyphens": { - "identifier": "replace", - "pattern": "-+$", - "replacement": "" - } - }, - "primaryOutputs": [ - { - "path": "apphost.cs" - } - ] -} diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/apphost.cs b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/apphost.cs deleted file mode 100644 index 663ef2a5745..00000000000 --- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/apphost.cs +++ /dev/null @@ -1,29 +0,0 @@ -#:sdk Aspire.AppHost.Sdk@!!REPLACE_WITH_LATEST_VERSION!! -#:package Aspire.Hosting.JavaScript@!!REPLACE_WITH_LATEST_VERSION!! -#:package Aspire.Hosting.Python@!!REPLACE_WITH_LATEST_VERSION!! -#if UseRedisCache -#:package Aspire.Hosting.Redis@!!REPLACE_WITH_LATEST_VERSION!! -#endif - -var builder = DistributedApplication.CreateBuilder(args); - -#if UseRedisCache -var cache = builder.AddRedis("cache"); - -#endif -var app = builder.AddUvicornApp("app", "./app", "main:app") - .WithUv() - .WithExternalHttpEndpoints() -#if UseRedisCache - .WithReference(cache) - .WaitFor(cache) -#endif - .WithHttpHealthCheck("/health"); - -var frontend = builder.AddViteApp("frontend", "./frontend") - .WithReference(app) - .WaitFor(app); - -app.PublishWithContainerFiles(frontend, "./static"); - -builder.Build().Run(); diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/apphost.run.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/apphost.run.json deleted file mode 100644 index 83df4bced7d..00000000000 --- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/apphost.run.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/launchsettings.json", - "profiles": { - //#if (HasHttpsProfile) - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - //#if (LocalhostTld) - "applicationUrl": "https://LocalhostTldHostNamePrefix.dev.localhost:17000;http://LocalhostTldHostNamePrefix.dev.localhost:15000", - //#else - "applicationUrl": "https://localhost:17000;http://localhost:15000", - //#endif - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DOTNET_ENVIRONMENT": "Development", - "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21000", - "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22000" - } - }, - //#endif - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - //#if (LocalhostTld) - "applicationUrl": "http://LocalhostTldHostNamePrefix.dev.localhost:15000", - //#else - "applicationUrl": "http://localhost:15000", - //#endif - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DOTNET_ENVIRONMENT": "Development", - "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19000", - "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20000" - } - } - } -} diff --git a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CaptureWorkspaceOnFailureAttribute.cs b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CaptureWorkspaceOnFailureAttribute.cs new file mode 100644 index 00000000000..8dc12d480a0 --- /dev/null +++ b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CaptureWorkspaceOnFailureAttribute.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using Xunit; +using Xunit.v3; + +namespace Aspire.Cli.EndToEnd.Tests.Helpers; + +/// +/// When applied to a test method, captures directories to +/// testresults/workspaces/{testName}/ on test failure so the generated +/// files are uploaded as CI artifacts for debugging. +/// +/// Register paths to capture via TestContext.Current.KeyValueStorage: +/// +/// "WorkspacePath" — the primary workspace directory +/// "CapturePath:{label}" — additional directories to capture under the given label +/// +/// Workspace capture is automatic when using . +/// +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +internal sealed class CaptureWorkspaceOnFailureAttribute : BeforeAfterTestAttribute +{ + public override void After(MethodInfo methodUnderTest, IXunitTest test) + { + if (TestContext.Current.TestState?.Result is not TestResult.Failed) + { + return; + } + + var testName = $"{test.TestCase.TestClassName}.{methodUnderTest.Name}"; + + try + { + // Capture primary workspace + if (TestContext.Current.KeyValueStorage.TryGetValue("WorkspacePath", out var value) && + value is string workspacePath && + Directory.Exists(workspacePath)) + { + CliE2ETestHelpers.CaptureDirectory(workspacePath, testName, label: null); + } + + // Capture additional registered paths (e.g., "CapturePath:aspire-home" → ~/.aspire) + foreach (var kvp in TestContext.Current.KeyValueStorage) + { + if (kvp.Key.StartsWith("CapturePath:", StringComparison.Ordinal) && + kvp.Value is string path && + Directory.Exists(path)) + { + var label = kvp.Key["CapturePath:".Length..]; + CliE2ETestHelpers.CaptureDirectory(path, testName, label); + } + } + } + catch + { + // Don't fail the test because of capture issues. + } + } +} diff --git a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs index 72fce5931f5..9312fba4c22 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs @@ -46,6 +46,11 @@ internal static async Task PrepareDockerEnvironmentAsync( await auto.TypeAsync($"cd /workspace/{workspace.WorkspaceRoot.Name}"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); + + // Set up EXIT trap to copy .aspire diagnostics to workspace for CI capture + await auto.TypeAsync($"trap 'cp -r ~/.aspire/logs /workspace/{workspace.WorkspaceRoot.Name}/.aspire-logs 2>/dev/null; cp -r ~/.aspire/packages /workspace/{workspace.WorkspaceRoot.Name}/.aspire-packages 2>/dev/null' EXIT"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); } } @@ -300,14 +305,29 @@ await auto.TypeAsync( throw new InvalidOperationException("aspire start failed. Check terminal output for CLI logs."); } - // Extract dashboard URL and verify it's reachable + // Extract dashboard URL and verify it's reachable. + // First check if the apphost crashed (aspire start exits 0 but prints error). await auto.TypeAsync( $"DASHBOARD_URL=$(sed -n " + - "'s/.*\"dashboardUrl\"[[:space:]]*:[[:space:]]*\"\\(https:\\/\\/localhost:[0-9]*\\).*/\\1/p' " + + "'s/.*\"dashboardUrl\"[[:space:]]*:[[:space:]]*\"\\(https\\?:\\/\\/localhost:[0-9]*\\).*/\\1/p' " + $"{jsonFile} | head -1)"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); + // If DASHBOARD_URL is empty, the apphost likely crashed — dump logs for diagnostics + await auto.TypeAsync( + "if [ -z \"$DASHBOARD_URL\" ]; then " + + "echo 'dashboard-url-empty'; " + + "echo '=== ASPIRE START JSON ==='; cat " + jsonFile + "; echo '=== END JSON ==='; " + + "echo '=== ALL LOGS ==='; ls -lt ~/.aspire/logs/ 2>/dev/null; echo '=== END LIST ==='; " + + "DETACH_LOG=$(ls -t ~/.aspire/logs/cli_*detach*.log 2>/dev/null | head -1); " + + "echo \"=== DETACH LOG: $DETACH_LOG ===\"; [ -n \"$DETACH_LOG\" ] && tail -200 \"$DETACH_LOG\"; echo '=== END DETACH ==='; " + + "CLI_LOG=$(ls -t ~/.aspire/logs/cli_*.log 2>/dev/null | head -1); " + + "echo \"=== CLI LOG: $CLI_LOG ===\"; [ -n \"$CLI_LOG\" ] && tail -100 \"$CLI_LOG\"; echo '=== END CLI ==='; " + + "fi"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); + await auto.TypeAsync( "curl -ksSL -o /dev/null -w 'dashboard-http-%{http_code}' \"$DASHBOARD_URL\" " + "|| echo 'dashboard-http-failed'"); @@ -327,4 +347,24 @@ internal static async Task AspireStopAsync( await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); } + + /// + /// Copies interesting diagnostic directories from ~/.aspire to the mounted workspace + /// so they are captured by . Call this before + /// exiting the container. Copies logs and NuGet restore output (libs directories). + /// + internal static async Task CaptureAspireDiagnosticsAsync( + this Hex1bTerminalAutomator auto, + SequenceCounter counter, + TemporaryWorkspace workspace) + { + var containerWorkspace = $"/workspace/{workspace.WorkspaceRoot.Name}"; + + // Copy CLI logs + await auto.TypeAsync($"cp -r ~/.aspire/logs {containerWorkspace}/.aspire-logs 2>/dev/null; " + + $"cp -r ~/.aspire/packages {containerWorkspace}/.aspire-packages 2>/dev/null; " + + "echo done"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); + } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs index 0216971c0f0..17f25e78934 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs @@ -376,4 +376,58 @@ internal static string ToContainerPath(string hostPath, TemporaryWorkspace works var relativePath = Path.GetRelativePath(workspace.WorkspaceRoot.FullName, hostPath); return $"/workspace/{workspace.WorkspaceRoot.Name}/" + relativePath.Replace('\\', '/'); } + + /// + /// Copies a directory to testresults/workspaces/{testName}/{label} for CI artifact upload. + /// Renames dot-prefixed directories to underscore-prefixed (upload-artifact skips hidden files). + /// + internal static void CaptureDirectory(string sourcePath, string testName, string? label) + { + var destDir = Path.Combine( + AppContext.BaseDirectory, + "TestResults", + "workspaces", + testName); + + if (label is not null) + { + destDir = Path.Combine(destDir, label); + } + + using var logWriter = new StreamWriter(Path.Combine( + Directory.CreateDirectory(destDir).FullName, + "_capture.log")); + + CopyDirectory(sourcePath, destDir, line => logWriter.WriteLine(line)); + } + + private static void CopyDirectory(string sourceDir, string destDir, Action? log) + { + Directory.CreateDirectory(destDir); + + log?.Invoke($"DIR: {sourceDir} ({Directory.GetFiles(sourceDir).Length} files, {Directory.GetDirectories(sourceDir).Length} dirs)"); + + foreach (var file in Directory.GetFiles(sourceDir)) + { + var destFile = Path.Combine(destDir, Path.GetFileName(file)); + File.Copy(file, destFile, overwrite: true); + } + + foreach (var dir in Directory.GetDirectories(sourceDir)) + { + var dirName = Path.GetFileName(dir); + + // Skip node_modules — too large for artifacts + if (dirName.Equals("node_modules", StringComparison.OrdinalIgnoreCase)) + { + log?.Invoke($" SKIP: {dirName}"); + continue; + } + + // Rename dot-prefixed dirs to underscore-prefixed + // (upload-artifact uses include-hidden-files: false by default) + var destDirName = dirName.StartsWith('.') ? "_" + dirName[1..] : dirName; + CopyDirectory(dir, Path.Combine(destDir, destDirName), log); + } + } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/PythonReactTemplateTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/PythonReactTemplateTests.cs index 729773b008d..9b80af4288f 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/PythonReactTemplateTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/PythonReactTemplateTests.cs @@ -4,7 +4,6 @@ using Aspire.Cli.EndToEnd.Tests.Helpers; using Aspire.Cli.Tests.Utils; using Hex1b.Automation; -using Hex1b.Input; using Xunit; namespace Aspire.Cli.EndToEnd.Tests; @@ -16,6 +15,7 @@ namespace Aspire.Cli.EndToEnd.Tests; public sealed class PythonReactTemplateTests(ITestOutputHelper output) { [Fact] + [CaptureWorkspaceOnFailure] public async Task CreateAndRunPythonReactProject() { var repoRoot = CliE2ETestHelpers.GetRepoRoot(); @@ -32,16 +32,20 @@ public async Task CreateAndRunPythonReactProject() await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliInDockerAsync(installMode, counter); + // Step 1: Create project using aspire new, selecting the FastAPI/React template await auto.AspireNewAsync("AspirePyReactApp", counter, template: AspireTemplate.PythonReact, useRedisCache: false); - // Run the project with aspire run - await auto.TypeAsync("aspire run"); + // Step 2: Navigate into the project directory so config resolution finds the + // project-level aspire.config.json (which has the packages section). + // See https://github.com/microsoft/aspire/issues/15623 + await auto.TypeAsync("cd AspirePyReactApp"); await auto.EnterAsync(); - await auto.WaitUntilTextAsync("Press CTRL+C to stop the apphost and exit.", timeout: TimeSpan.FromMinutes(2)); - - await auto.Ctrl().KeyAsync(Hex1bKey.C); await auto.WaitForSuccessPromptAsync(counter); + // Step 3: Start and stop the project + await auto.AspireStartAsync(counter); + await auto.AspireStopAsync(counter); + await auto.TypeAsync("exit"); await auto.EnterAsync(); diff --git a/tests/Aspire.Cli.Tests/Templating/ConditionalBlockProcessorTests.cs b/tests/Aspire.Cli.Tests/Templating/ConditionalBlockProcessorTests.cs new file mode 100644 index 00000000000..44969530ce4 --- /dev/null +++ b/tests/Aspire.Cli.Tests/Templating/ConditionalBlockProcessorTests.cs @@ -0,0 +1,457 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Cli.Templating; + +namespace Aspire.Cli.Tests.Templating; + +public class ConditionalBlockProcessorTests +{ + // ────────────────────────────────────────────────────────────────────── + // Basic include / exclude + // ────────────────────────────────────────────────────────────────────── + + [Fact] + public void Include_KeepsContent_RemovesMarkers() + { + var input = """ + before + // {{#feature}} + included content + // {{/feature}} + after + """; + + var result = ConditionalBlockProcessor.ProcessBlock(input, "feature", include: true); + + Assert.Contains("included content", result); + Assert.DoesNotContain("{{#feature}}", result); + Assert.DoesNotContain("{{/feature}}", result); + Assert.Contains("before", result); + Assert.Contains("after", result); + } + + [Fact] + public void Exclude_RemovesContentAndMarkers() + { + var input = """ + before + // {{#feature}} + excluded content + // {{/feature}} + after + """; + + var result = ConditionalBlockProcessor.ProcessBlock(input, "feature", include: false); + + Assert.DoesNotContain("excluded content", result); + Assert.DoesNotContain("{{#feature}}", result); + Assert.DoesNotContain("{{/feature}}", result); + Assert.Contains("before", result); + Assert.Contains("after", result); + } + + // ────────────────────────────────────────────────────────────────────── + // Comment style variations + // ────────────────────────────────────────────────────────────────────── + + [Fact] + public void Handles_DoubleSlash_CommentMarkers() + { + var input = "line1\n// {{#test}}\nkept\n// {{/test}}\nline2\n"; + + var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: true); + + Assert.Equal("line1\nkept\nline2\n", result); + } + + [Fact] + public void Handles_Hash_CommentMarkers() + { + var input = "line1\n# {{#test}}\nkept\n# {{/test}}\nline2\n"; + + var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: true); + + Assert.Equal("line1\nkept\nline2\n", result); + } + + [Fact] + public void Handles_Bare_Markers_WithoutCommentPrefix() + { + var input = "line1\n{{#test}}\nkept\n{{/test}}\nline2\n"; + + var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: true); + + Assert.Equal("line1\nkept\nline2\n", result); + } + + [Fact] + public void Handles_IndentedMarkers() + { + var input = "line1\n // {{#test}}\n kept\n // {{/test}}\nline2\n"; + + var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: true); + + Assert.Equal("line1\n kept\nline2\n", result); + } + + // ────────────────────────────────────────────────────────────────────── + // Multiple blocks of same type + // ────────────────────────────────────────────────────────────────────── + + [Fact] + public void Include_MultipleBlocks_AllKept() + { + var input = "a\n// {{#x}}\nb\n// {{/x}}\nc\n// {{#x}}\nd\n// {{/x}}\ne\n"; + + var result = ConditionalBlockProcessor.ProcessBlock(input, "x", include: true); + + Assert.Equal("a\nb\nc\nd\ne\n", result); + } + + [Fact] + public void Exclude_MultipleBlocks_AllRemoved() + { + var input = "a\n// {{#x}}\nb\n// {{/x}}\nc\n// {{#x}}\nd\n// {{/x}}\ne\n"; + + var result = ConditionalBlockProcessor.ProcessBlock(input, "x", include: false); + + Assert.Equal("a\nc\ne\n", result); + } + + // ────────────────────────────────────────────────────────────────────── + // Edge cases: position in file + // ────────────────────────────────────────────────────────────────────── + + [Fact] + public void Block_AtStartOfFile() + { + // Exercises LastIndexOf returning -1 for start-of-file boundary. + var input = "// {{#test}}\nfirst\n// {{/test}}\nafter\n"; + + var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: true); + + Assert.Equal("first\nafter\n", result); + } + + [Fact] + public void Block_AtEndOfFile_NoTrailingNewline() + { + // Exercises IndexOf returning content.Length for end-of-file boundary. + var input = "before\n// {{#test}}\nlast\n// {{/test}}"; + + var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: true); + + Assert.Equal("before\nlast\n", result); + } + + [Fact] + public void Block_IsEntireFile_Exclude() + { + var input = "// {{#test}}\nall content\n// {{/test}}\n"; + + var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: false); + + Assert.Equal("", result); + } + + // ────────────────────────────────────────────────────────────────────── + // Edge cases: content variations + // ────────────────────────────────────────────────────────────────────── + + [Fact] + public void EmptyBlock_ProducesSameResultForIncludeAndExclude() + { + var input = "before\n// {{#test}}\n// {{/test}}\nafter\n"; + + var includeResult = ConditionalBlockProcessor.ProcessBlock(input, "test", include: true); + var excludeResult = ConditionalBlockProcessor.ProcessBlock(input, "test", include: false); + + Assert.Equal("before\nafter\n", includeResult); + Assert.Equal(includeResult, excludeResult); + } + + [Fact] + public void MultilineBlockContent_Include() + { + var input = "before\n// {{#test}}\nline1\nline2\nline3\n// {{/test}}\nafter\n"; + + var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: true); + + Assert.Equal("before\nline1\nline2\nline3\nafter\n", result); + } + + [Fact] + public void ContentWithCurlyBraces_NotConfusedWithMarkers() + { + var input = "before\n// {{#test}}\nvar x = { key: \"value\" };\n// {{/test}}\nafter\n"; + + var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: true); + + Assert.Equal("before\nvar x = { key: \"value\" };\nafter\n", result); + } + + [Fact] + public void ContentWithTemplateTokens_NotConfusedWithMarkers() + { + var input = "before\n// {{#test}}\nname: {{projectName}}\n// {{/test}}\nafter\n"; + + var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: true); + + Assert.Equal("before\nname: {{projectName}}\nafter\n", result); + } + + // ────────────────────────────────────────────────────────────────────── + // No markers / no matching markers + // ────────────────────────────────────────────────────────────────────── + + [Fact] + public void NoMarkers_ReturnsUnchanged() + { + var input = "just some content\nwith no markers\n"; + + var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: true); + + Assert.Equal(input, result); + } + + [Fact] + public void DifferentBlockName_IgnoresNonMatchingMarkers() + { + var input = "before\n// {{#other}}\nkept\n// {{/other}}\nafter\n"; + + var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: false); + + Assert.Equal(input, result); + } + + [Fact] + public void MissingEndMarker_ThrowsInvalidOperationException() + { + var input = "before\n// {{#test}}\ncontent\nafter\n"; + + Assert.Throws( + () => ConditionalBlockProcessor.ProcessBlock(input, "test", include: false)); + } + + [Fact] + public void MissingStartMarker_LeavesContentUnchanged() + { + var input = "before\ncontent\n// {{/test}}\nafter\n"; + + var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: false); + + Assert.Equal(input, result); + } + + [Fact] + public void EmptyContent_ReturnsEmpty() + { + var result = ConditionalBlockProcessor.ProcessBlock("", "test", include: true); + + Assert.Equal("", result); + } + + // ────────────────────────────────────────────────────────────────────── + // Process() with multiple conditions + // ────────────────────────────────────────────────────────────────────── + + [Fact] + public void Process_MultipleConditions_IncludesAndExcludesCorrectly() + { + var input = """ + start + // {{#redis}} + redis stuff + // {{/redis}} + middle + // {{#no-redis}} + no redis stuff + // {{/no-redis}} + end + """; + + var conditions = new Dictionary + { + ["redis"] = true, + ["no-redis"] = false, + }; + + var result = ConditionalBlockProcessor.Process(input, conditions); + + Assert.Contains("redis stuff", result); + Assert.DoesNotContain("no redis stuff", result); + Assert.Contains("start", result); + Assert.Contains("middle", result); + Assert.Contains("end", result); + } + + [Fact] + public void Process_InverseConditions_IncludesAndExcludesCorrectly() + { + var input = """ + start + // {{#redis}} + redis stuff + // {{/redis}} + middle + // {{#no-redis}} + no redis stuff + // {{/no-redis}} + end + """; + + var conditions = new Dictionary + { + ["redis"] = false, + ["no-redis"] = true, + }; + + var result = ConditionalBlockProcessor.Process(input, conditions); + + // "no redis stuff" contains the substring "redis stuff", so assert on the full line. + Assert.DoesNotContain("// {{#redis}}", result); + Assert.Contains("no redis stuff", result); + Assert.Contains("start", result); + Assert.Contains("middle", result); + Assert.Contains("end", result); + } + + // ────────────────────────────────────────────────────────────────────── + // Adjacent blocks + // ────────────────────────────────────────────────────────────────────── + + [Fact] + public void AdjacentBlocks_ExcludeBoth() + { + var input = "before\n// {{#a}}\nalpha\n// {{/a}}\n// {{#b}}\nbeta\n// {{/b}}\nafter\n"; + + var conditions = new Dictionary + { + ["a"] = false, + ["b"] = false, + }; + + var result = ConditionalBlockProcessor.Process(input, conditions); + + Assert.DoesNotContain("alpha", result); + Assert.DoesNotContain("beta", result); + Assert.Equal("before\nafter\n", result); + } + + // ────────────────────────────────────────────────────────────────────── + // Realistic template scenarios + // ────────────────────────────────────────────────────────────────────── + + [Fact] + public void PythonFile_WithRedisAndNoRedisBlocks() + { + var input = """ + import os + # {{#redis}} + import redis + # {{/redis}} + + # {{#redis}} + def get_client(): + return redis.from_url(os.environ.get("CACHE_URI")) + # {{/redis}} + + # {{#no-redis}} + def get_forecast(): + return generate_fresh() + # {{/no-redis}} + """; + + var conditions = new Dictionary + { + ["redis"] = true, + ["no-redis"] = false, + }; + + var result = ConditionalBlockProcessor.Process(input, conditions); + + Assert.Contains("import redis", result); + Assert.Contains("def get_client():", result); + Assert.DoesNotContain("def get_forecast():", result); + Assert.DoesNotContain("{{#", result); + Assert.DoesNotContain("{{/", result); + } + + [Fact] + public void PyprojectToml_DependenciesBlock() + { + var input = """ + dependencies = [ + "fastapi[standard]>=0.119.0", + "opentelemetry-distro>=0.59b0", + # {{#redis}} + "opentelemetry-instrumentation-redis>=0.59b0", + "redis>=6.4.0", + # {{/redis}} + ] + """; + + var resultWith = ConditionalBlockProcessor.ProcessBlock(input, "redis", include: true); + Assert.Contains("redis>=6.4.0", resultWith); + Assert.DoesNotContain("{{#redis}}", resultWith); + // Verify the closing bracket is still there + Assert.Contains("]", resultWith); + + var resultWithout = ConditionalBlockProcessor.ProcessBlock(input, "redis", include: false); + Assert.DoesNotContain("redis", resultWithout); + Assert.Contains("fastapi", resultWithout); + Assert.Contains("]", resultWithout); + } + + // ────────────────────────────────────────────────────────────────────── + // Whitespace / formatting preservation + // ────────────────────────────────────────────────────────────────────── + + [Fact] + public void PreservesIndentation_InKeptContent() + { + var input = " // {{#test}}\n indented content\n // {{/test}}\n"; + + var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: true); + + Assert.Equal(" indented content\n", result); + } + + [Fact] + public void SurroundingBlankLines_PreservedWhenExcluding() + { + var input = "before\n\n// {{#test}}\ncontent\n// {{/test}}\n\nafter\n"; + + var result = ConditionalBlockProcessor.ProcessBlock(input, "test", include: false); + + // The processor removes the block and its markers but preserves surrounding blank lines. + Assert.Equal("before\n\n\nafter\n", result); + } + + // ────────────────────────────────────────────────────────────────────── + // Block name edge cases + // ────────────────────────────────────────────────────────────────────── + + [Fact] + public void BlockNameWithHyphen_WorksCorrectly() + { + var input = "before\n// {{#no-redis}}\ncontent\n// {{/no-redis}}\nafter\n"; + + var result = ConditionalBlockProcessor.ProcessBlock(input, "no-redis", include: true); + + Assert.Equal("before\ncontent\nafter\n", result); + } + + [Fact] + public void SimilarBlockNames_DoNotInterfere() + { + var input = "// {{#redis}}\nredis content\n// {{/redis}}\n// {{#redis-cluster}}\ncluster content\n// {{/redis-cluster}}\n"; + + var result = ConditionalBlockProcessor.ProcessBlock(input, "redis", include: false); + + Assert.DoesNotContain("redis content", result); + Assert.Contains("cluster content", result); + Assert.Contains("{{#redis-cluster}}", result); + } +} diff --git a/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs b/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs index 3bf653368cc..b3b801f5668 100644 --- a/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs +++ b/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs @@ -324,7 +324,7 @@ public async Task GetTemplates_SingleFileAppHostIsNotReturned() // Assert var templateNames = templates.Select(t => t.Name).ToList(); Assert.DoesNotContain("aspire-apphost-singlefile", templateNames); - Assert.Contains("aspire-py-starter", templateNames); + Assert.DoesNotContain("aspire-py-starter", templateNames); } [Fact] diff --git a/tests/Aspire.Templates.Tests/LocalhostTldHostnameTests.cs b/tests/Aspire.Templates.Tests/LocalhostTldHostnameTests.cs index 6fa3c1c03c1..4ed489c2461 100644 --- a/tests/Aspire.Templates.Tests/LocalhostTldHostnameTests.cs +++ b/tests/Aspire.Templates.Tests/LocalhostTldHostnameTests.cs @@ -23,7 +23,6 @@ public partial class LocalhostTldHostnameTests(ITestOutputHelper testOutput) : T { "aspire-apphost", "my.service.name", "my-service-name" }, { "aspire-apphost-singlefile", "-my.service..name-", "my-service-name" }, { "aspire-starter", "Test_App.1", "test-app-1" }, - { "aspire-py-starter", "xn--Test_App_1", "xn-test-app-1" }, { "aspire-ts-cs-starter", "My-App.Test", "my-app-test" } }; @@ -36,7 +35,7 @@ public async Task LocalhostTld_GeneratesDnsCompliantHostnames(string templateNam var targetFramework = templateName switch { - "aspire-apphost-singlefile" or "aspire-py-starter" => TestTargetFramework.None, // These templates do not support -f argument + "aspire-apphost-singlefile" => TestTargetFramework.None, // These templates do not support -f argument _ => TestTargetFramework.Next // LocalhostTld only available on net10.0 }; @@ -57,7 +56,7 @@ public async Task LocalhostTld_GeneratesDnsCompliantHostnames(string templateNam "aspire-ts-cs-starter" or "aspire-starter" => Path.Combine(project.RootDir, $"{projectName}.AppHost", "Properties", "launchSettings.json"), "aspire" => Path.Combine(project.RootDir, $"{projectName}.AppHost", "Properties", "launchSettings.json"), "aspire-apphost" => Path.Combine(project.RootDir, "Properties", "launchSettings.json"), - "aspire-apphost-singlefile" or "aspire-py-starter" => Path.Combine(project.RootDir, "apphost.run.json"), + "aspire-apphost-singlefile" => Path.Combine(project.RootDir, "apphost.run.json"), _ => throw new ArgumentException($"Unknown template: {templateName}") }; diff --git a/tests/Shared/Docker/Dockerfile.e2e b/tests/Shared/Docker/Dockerfile.e2e index 4ce1c4f2e6a..ec4a952cb4f 100644 --- a/tests/Shared/Docker/Dockerfile.e2e +++ b/tests/Shared/Docker/Dockerfile.e2e @@ -65,10 +65,12 @@ RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ apt-get install -y --no-install-recommends nodejs && \ rm -rf /var/lib/apt/lists/* -# Install Python (needed for Python templates). +# Install Python and uv (needed for Python templates). RUN apt-get update -qq && \ apt-get install -y --no-install-recommends python3 python3-pip python3-venv && \ rm -rf /var/lib/apt/lists/* +RUN curl -LsSf https://astral.sh/uv/install.sh | sh +ENV PATH="/root/.local/bin:${PATH}" # Install Java (needed for Java AppHost tests). RUN apt-get update -qq && \ diff --git a/tests/Shared/TemporaryRepo.cs b/tests/Shared/TemporaryRepo.cs index e61ff2d4c48..b68046d7a4e 100644 --- a/tests/Shared/TemporaryRepo.cs +++ b/tests/Shared/TemporaryRepo.cs @@ -68,6 +68,9 @@ internal static TemporaryWorkspace Create(ITestOutputHelper outputHelper) var aspireDir = Directory.CreateDirectory(Path.Combine(path, ".aspire")); File.WriteAllText(Path.Combine(aspireDir.FullName, "settings.json"), "{}"); + // Register workspace path for CaptureWorkspaceOnFailure attribute + TestContext.Current?.KeyValueStorage["WorkspacePath"] = repoDirectory.FullName; + return new TemporaryWorkspace(outputHelper, repoDirectory); } }