From 3113f6f9a7285d3663909156012fd3b1f445d234 Mon Sep 17 00:00:00 2001 From: Omnideth Date: Sun, 24 May 2026 21:37:26 -0500 Subject: [PATCH 01/11] Plan made, will remove later. --- perl-typescript-support-plan.md | 135 ++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 perl-typescript-support-plan.md diff --git a/perl-typescript-support-plan.md b/perl-typescript-support-plan.md new file mode 100644 index 000000000..2ef537127 --- /dev/null +++ b/perl-typescript-support-plan.md @@ -0,0 +1,135 @@ +# Perl TypeScript AppHost Support Plan + +## Goal + +Enable `CommunityToolkit.Aspire.Hosting.Perl` to work from a TypeScript AppHost, using the current `examples/perl/cpanm-api-integration` sample as the first validated scenario. + +## What The Aspire Docs Say + +- `aspire docs get multi-language-integrations`: + - TypeScript AppHost support comes from ATS annotations on the .NET hosting integration. + - The CLI scans `[AspireExport]` methods and types, then generates the TypeScript SDK into `.modules/`. + - `Aspire.Hosting.Integration.Analyzers` is the recommended build-time guardrail for export mistakes. + - Capability IDs must be unique; when needed, use distinct export IDs plus `MethodName` to keep the generated TypeScript names clean. +- `aspire docs get multi-language-architecture`: + - TypeScript AppHosts are guest processes that talk to the .NET host over local JSON-RPC. + - We do not hand-write TypeScript bindings; the SDK is generated from the exported C# surface. +- `aspire docs get typescript-apphost-project-structure`: + - A TypeScript AppHost needs `apphost.ts`, `aspire.config.json`, `package.json`, and `tsconfig.json`. + - Local development can reference a hosting integration by `.csproj` path in `aspire.config.json`. + - `.modules/` is generated and should not be edited directly. + +## Repo Findings + +- The Perl integration currently has no ATS annotations in `src/CommunityToolkit.Aspire.Hosting.Perl/**`. +- The current Perl sample logic lives in `examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost/AppHost.cs` and uses: + - `AddPerlApi` + - `WithCpanMinus` + - `WithPackage` + - `WithLocalLib` + - `AddPerlScript` + - `WithEnvironment` + - `WithReference` + - `WaitFor` +- Existing integrations with TypeScript support follow two patterns: + - Export add-methods with `[AspireExport(...)]`. + - Add ATS-friendly overloads or `[AspireExportIgnore]` when the public C# overload is not suitable for polyglot callers. +- Shared TypeScript validation already exists in `tests/CommunityToolkit.Aspire.Testing/TypeScriptAppHostTest.cs`, so Perl only needs a new test case instead of new harness code. +- `Directory.Packages.props` manages package versions centrally and does not currently include `Aspire.Hosting.Integration.Analyzers`. + +## Implementation Plan + +1. Enable ATS in the Perl integration project. + - Add `Aspire.Hosting.Integration.Analyzers` to `Directory.Packages.props`. + - Add a `PackageReference` for the analyzer in `src/CommunityToolkit.Aspire.Hosting.Perl/CommunityToolkit.Aspire.Hosting.Perl.csproj` with private/analyzer-only assets. + - Suppress `ASPIREATS001` in the Perl project file, matching the pattern used by other ATS-exporting integrations. + +2. Export the Perl surface needed by a TypeScript AppHost. + - Add `[AspireExport]` to the entry points in `src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.cs`: + - `AddPerlScript` + - `AddPerlApi` + - `AddPerlModule` + - `AddPerlExecutable` + - Export the fluent methods required by the first sample: + - `WithCpanMinus` + - `WithPackage` + - `WithLocalLib` + - Strong candidates for broader parity after the first pass: + - `WithCarton` + - `WithProjectDependencies` + - `WithPerlbrew` / `WithPerlbrewEnvironment` + - Expect to validate whether ATS accepts the current generic Perl fluent methods as-is. + - If the analyzer rejects them or the generated surface is awkward, add concrete `PerlAppResource` polyglot overloads. + - Use distinct export IDs and `MethodName` where needed. + - Use `[AspireExportIgnore]` on overloads that should stay C#-only. + +3. Add the first TypeScript AppHost example. + - Create `examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/`. + - Include: + - `apphost.ts` + - `aspire.config.json` + - `package.json` + - `tsconfig.json` + - `eslint.config.mjs` + - `package-lock.json` if we keep parity with the committed TypeScript examples already in the repo + - In `aspire.config.json`, reference the local integration project: + - `CommunityToolkit.Aspire.Hosting.Perl`: `../../../src/CommunityToolkit.Aspire.Hosting.Perl/CommunityToolkit.Aspire.Hosting.Perl.csproj` + - Mirror the current C# sample behavior: + - Perl API resource using `cpanm`, `Mojolicious::Lite`, `local::lib`, and an HTTP endpoint + - Perl driver script with environment/reference/wait wiring + - One implementation detail to verify after export generation: + - Confirm the generated endpoint handle syntax for wiring the API URL into `withEnvironment` by inspecting `.modules/aspire.ts` after `aspire restore`. + +4. Add TypeScript AppHost validation. + - Add `tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs`. + - Use the shared harness with: + - `appHostProject: "CpanmApiIntegration.AppHost.TypeScript"` + - `packageName: "CommunityToolkit.Aspire.Hosting.Perl"` + - `exampleName: "perl/cpanm-api-integration"` + - Initial validation settings should be: + - `waitForResources: ["perl-api"]` + - `waitStatus: "up"` + - `requiredCommands: ["perl", "cpanm"]` + - Rationale: + - The sample API has an HTTP endpoint but no explicit health check, so `up` is safer than `healthy`. + - The driver script may be short-lived, so it is not the best primary wait target for the first pass. + - No test project file change should be needed unless we discover explicit compile includes later. + +5. Validate the end-to-end flow. + - Build the Perl integration project. + - Run the Perl test project. + - Run the new `TypeScriptAppHostTests`. + - In the TypeScript example directory: + - `npm ci` + - `aspire restore` + - inspect `.modules/aspire.ts` + - `npx tsc --noEmit` + - Confirm there are no ATS analyzer collisions and that the generated TypeScript names match the intended API shape. + +## Recommended Cut Line + +- Minimum viable TypeScript support for the first implementation pass: + - ATS analyzer wiring + - export the add-methods plus the three fluent methods used by the sample (`WithCpanMinus`, `WithPackage`, `WithLocalLib`) + - create the TypeScript example + - add the TypeScript AppHost test +- Defer until the first slice is green: + - exporting the full Perl surface + - `Perlbrew` support in TypeScript + - certificate-trust/export edge cases + - extra sample permutations beyond `cpanm-api-integration` + +## Risks And Open Questions + +- The main technical risk is ATS compatibility of the current generic Perl fluent methods. +- If `WithPackage` or `WithProjectDependencies` generate an awkward TypeScript signature, we may want dedicated polyglot overloads instead of exporting the raw C# shape. +- If the generated SDK does not make endpoint values directly consumable for `withEnvironment`, the sample may need a small adjustment from the current C# AppHost. +- Because analyzer versions are managed centrally in this repo, the package-version change will likely need to happen in `Directory.Packages.props`, not just the Perl project. + +## First Implementation Order + +1. Add analyzer/version plumbing. +2. Export `AddPerlApi`, `AddPerlScript`, `WithCpanMinus`, `WithPackage`, and `WithLocalLib`. +3. Generate the TypeScript example and inspect `.modules/aspire.ts`. +4. Adjust export shapes only if the analyzer or generated SDK forces it. +5. Add the TypeScript AppHost test and validate it. \ No newline at end of file From e561d39410c53e2f1664b0fea722f316f22365b1 Mon Sep 17 00:00:00 2001 From: Matthew Austew Date: Sun, 24 May 2026 23:52:11 -0500 Subject: [PATCH 02/11] Large chunk of functionality exposed in Typescript for MVP. Will expand more before PR. --- .gitignore | 2 + eng/testing/validate-typescript-apphost.ps1 | 171 ++ .../apphost.ts | 16 + .../aspire.config.json | 22 + .../eslint.config.mjs | 17 + .../package-lock.json | 2039 +++++++++++++++++ .../package.json | 28 + .../tsconfig.json | 20 + perl-typescript-support-plan.md | 18 +- perl-typescript-support-roadmap.md | 430 ++++ ...esourceBuilderExtensions.PackageManager.cs | 4 +- .../PerlAppResourceBuilderExtensions.cs | 6 +- .../TypeScriptAppHostTests.cs | 20 + .../TypeScriptAppHostTest.cs | 7 + 14 files changed, 2786 insertions(+), 14 deletions(-) create mode 100644 examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/apphost.ts create mode 100644 examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/aspire.config.json create mode 100644 examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/eslint.config.mjs create mode 100644 examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/package-lock.json create mode 100644 examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/package.json create mode 100644 examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/tsconfig.json create mode 100644 perl-typescript-support-roadmap.md create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs diff --git a/.gitignore b/.gitignore index 09a659f83..6c3e54926 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,8 @@ node_modules examples/perl/**/local/* **cpanfile.snapshot **/.aspire/modules/ +**/.modules/ +**/*.AppHost.TypeScript/.aspire/ **/*.AppHost.TypeScript/nuget.config tsconfig.apphost.json .ngrok diff --git a/eng/testing/validate-typescript-apphost.ps1 b/eng/testing/validate-typescript-apphost.ps1 index bebaba063..1db17b9b9 100644 --- a/eng/testing/validate-typescript-apphost.ps1 +++ b/eng/testing/validate-typescript-apphost.ps1 @@ -4,10 +4,19 @@ param( [Parameter(Mandatory = $true)] [string]$AppHostPath, + [string]$PackageProjectPath = "", + + [Parameter(Mandatory = $true)] + [string]$PackageName, + + [switch]$UseConfiguredPackages, + [string[]]$WaitForResources = @(), [string[]]$RequiredCommands = @(), + [string]$PackageVersion = "", + [ValidateSet("healthy", "up", "down")] [string]$WaitStatus = "healthy", @@ -85,12 +94,97 @@ function Invoke-CleanupStep { } } +function Remove-PathWithRetry { + param( + [Parameter(Mandatory = $true)] + [string]$Path, + + [switch]$Recurse + ) + + if (-not (Test-Path -LiteralPath $Path)) { + return + } + + $maxAttempts = 6 + for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) { + try { + if ($Recurse) { + Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction Stop + } + else { + Remove-Item -LiteralPath $Path -Force -ErrorAction Stop + } + + return + } + catch { + if ($attempt -eq $maxAttempts) { + throw + } + + Start-Sleep -Milliseconds (250 * $attempt) + } + } +} + $resolvedAppHostPath = (Resolve-Path $AppHostPath).Path +$resolvedPackageProjectPath = $null +$localSource = $null + +if (-not $UseConfiguredPackages) { + if ([string]::IsNullOrWhiteSpace($PackageProjectPath)) { + throw "PackageProjectPath is required unless -UseConfiguredPackages is set." + } + + $resolvedPackageProjectPath = (Resolve-Path $PackageProjectPath).Path +} + $appHostDirectory = Split-Path -Parent $resolvedAppHostPath +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\\..")).Path +$configPath = Join-Path $appHostDirectory "aspire.config.json" +$nugetConfigPath = Join-Path $appHostDirectory "nuget.config" +$generatedRestoreRoot = Join-Path $appHostDirectory ".aspire\integrations\package-restore" +$generatedRestoreNugetConfigPath = Join-Path $generatedRestoreRoot "nuget.config" +$originalConfig = $null $appStarted = $false $primaryError = $null $cleanupFailures = [System.Collections.Generic.List[string]]::new() +if (-not $UseConfiguredPackages) { + $localSource = Join-Path ([System.IO.Path]::GetTempPath()) ("ct-polyglot-" + [Guid]::NewGuid().ToString("N")) + + if ([string]::IsNullOrWhiteSpace($PackageVersion)) { + $versionPrefix = (& dotnet msbuild $resolvedPackageProjectPath -nologo -v:q -getProperty:VersionPrefix).Trim() + if ([string]::IsNullOrWhiteSpace($versionPrefix)) { + throw "Could not determine the evaluated VersionPrefix for $resolvedPackageProjectPath." + } + + $PackageVersion = "$versionPrefix-polyglot.local" + } +} + +# Discover local CommunityToolkit project references that also need packing +$localDependencies = @() +if (-not $UseConfiguredPackages) { + $projRefJson = (& dotnet msbuild $resolvedPackageProjectPath -nologo -v:q -getItem:ProjectReference) | Out-String + $projRefData = $projRefJson | ConvertFrom-Json + $projRefs = @($projRefData.Items.ProjectReference) + foreach ($ref in $projRefs) { + if ($ref.Filename -like "CommunityToolkit.*") { + $localDependencies += @{ + Name = $ref.Filename + FullPath = $ref.FullPath + } + } + } +} + +if ($localDependencies.Count -gt 0) { + $depNames = ($localDependencies | ForEach-Object { $_.Name }) -join ", " + Write-Host "Discovered local dependencies to pack: $depNames" +} + if ($WaitForResources.Count -eq 1 -and -not [string]::IsNullOrWhiteSpace($WaitForResources[0])) { $splitOptions = [System.StringSplitOptions]::RemoveEmptyEntries -bor [System.StringSplitOptions]::TrimEntries $WaitForResources = $WaitForResources[0].Split(",", $splitOptions) @@ -136,6 +230,66 @@ foreach ($secret in $Secrets) { } try { + $originalConfig = Get-Content -Path $configPath -Raw + if (-not $UseConfiguredPackages) { + New-Item -ItemType Directory -Path $localSource -Force | Out-Null + + Invoke-ExternalCommand "dotnet" @( + "pack", + $resolvedPackageProjectPath, + "-c", "Debug", + "-p:PackageVersion=$PackageVersion", + "-o", $localSource + ) + + foreach ($dep in $localDependencies) { + Invoke-ExternalCommand "dotnet" @( + "pack", + $dep.FullPath, + "-c", "Debug", + "-p:PackageVersion=$PackageVersion", + "-o", $localSource + ) + } + + $config = $originalConfig | ConvertFrom-Json -AsHashtable + if ($null -eq $config["packages"]) { + $config["packages"] = [ordered]@{} + } + + $config["packages"][$PackageName] = $PackageVersion + foreach ($dep in $localDependencies) { + $config["packages"][$dep.Name] = $PackageVersion + } + $config | ConvertTo-Json -Depth 10 | Set-Content -Path $configPath -NoNewline + + $temporaryNugetConfig = @" + + + + + + + + + + + + + + + + + + + +"@ + + $temporaryNugetConfig | Set-Content -Path $nugetConfigPath -NoNewline + New-Item -ItemType Directory -Path $generatedRestoreRoot -Force | Out-Null + $temporaryNugetConfig | Set-Content -Path $generatedRestoreNugetConfigPath -NoNewline + } + Push-Location $appHostDirectory try { Invoke-ExternalCommand "npm" @("ci") @@ -226,6 +380,23 @@ finally { ) } } -Failures $cleanupFailures + + Invoke-CleanupStep -Description "restore Aspire config" -Action { + if ($null -ne $originalConfig) { + Set-Content -Path $configPath -Value $originalConfig -NoNewline + } + } -Failures $cleanupFailures + + Invoke-CleanupStep -Description "remove generated nuget.config" -Action { + Remove-PathWithRetry -Path $nugetConfigPath + Remove-PathWithRetry -Path $generatedRestoreNugetConfigPath + } -Failures $cleanupFailures + + Invoke-CleanupStep -Description "remove local package source" -Action { + if (-not [string]::IsNullOrWhiteSpace($localSource)) { + Remove-PathWithRetry -Path $localSource -Recurse + } + } -Failures $cleanupFailures } if ($cleanupFailures.Count -gt 0) { diff --git a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/apphost.ts b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/apphost.ts new file mode 100644 index 000000000..251fd1ed9 --- /dev/null +++ b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/apphost.ts @@ -0,0 +1,16 @@ +import { createBuilder } from "./.modules/aspire.js"; + +const builder = await createBuilder(); + +const perlApi = await builder.addPerlApi("perl-api", ".", "../scripts/API.pl"); +await perlApi.withCpanMinus(); +await perlApi.withPackage("Mojolicious::Lite", { force: true, skipTest: true }); +await perlApi.withLocalLib({ path: "local" }); +await perlApi.withHttpEndpoint({ name: "http", env: "PORT" }); + +const perlDriver = await builder.addPerlScript("perl-driver", "../scripts", "driver.pl"); +await perlDriver.withEnvironment("API_URL", perlApi.getEndpoint("http")); +await perlDriver.withReference(perlApi); +await perlDriver.waitFor(perlApi); + +await builder.build().run(); \ No newline at end of file diff --git a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/aspire.config.json b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/aspire.config.json new file mode 100644 index 000000000..d6aeb73a6 --- /dev/null +++ b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/aspire.config.json @@ -0,0 +1,22 @@ +{ + "appHost": { + "path": "apphost.ts", + "language": "typescript/nodejs" + }, + "sdk": { + "version": "13.3.0" + }, + "profiles": { + "https": { + "applicationUrl": "https://localhost:17453;http://localhost:15453", + "environmentVariables": { + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21453", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22453" + } + } + }, + "channel": "stable", + "packages": { + "CommunityToolkit.Aspire.Hosting.Perl": "../../../../src/CommunityToolkit.Aspire.Hosting.Perl/CommunityToolkit.Aspire.Hosting.Perl.csproj" + } +} \ No newline at end of file diff --git a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/eslint.config.mjs b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/eslint.config.mjs new file mode 100644 index 000000000..73a2cb305 --- /dev/null +++ b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/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 }], + }, +}); \ No newline at end of file diff --git a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/package-lock.json b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/package-lock.json new file mode 100644 index 000000000..b54a33af7 --- /dev/null +++ b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/package-lock.json @@ -0,0 +1,2039 @@ +{ + "name": "communitytoolkit-aspire-hosting-perl-apphost-typescript", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "communitytoolkit-aspire-hosting-perl-apphost-typescript", + "version": "1.0.0", + "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.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "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.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.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.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "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.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz", + "integrity": "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/type-utils": "8.59.4", + "@typescript-eslint/utils": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.4", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.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.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.4.tgz", + "integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.4.tgz", + "integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.4", + "@typescript-eslint/types": "^8.59.4", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz", + "integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4" + }, + "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.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz", + "integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz", + "integrity": "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/utils": "8.59.4", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz", + "integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==", + "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.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz", + "integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.4", + "@typescript-eslint/tsconfig-utils": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.4.tgz", + "integrity": "sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz", + "integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "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", + "peer": true, + "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.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "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.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "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.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "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.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz", + "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.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.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.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.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "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/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.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "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.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "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/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "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.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "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.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "peer": true, + "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.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsx": { + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", + "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "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", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.4.tgz", + "integrity": "sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.4", + "@typescript-eslint/parser": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/utils": "8.59.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "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/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/package.json b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/package.json new file mode 100644 index 000000000..b67178aee --- /dev/null +++ b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/package.json @@ -0,0 +1,28 @@ +{ + "name": "communitytoolkit-aspire-hosting-perl-apphost-typescript", + "version": "1.0.0", + "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" + } +} \ No newline at end of file diff --git a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/tsconfig.json b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/tsconfig.json new file mode 100644 index 000000000..8b748a15a --- /dev/null +++ b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "outDir": "./dist", + "rootDir": "." + }, + "include": [ + "apphost.ts", + ".modules/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/perl-typescript-support-plan.md b/perl-typescript-support-plan.md index 2ef537127..bbda29d14 100644 --- a/perl-typescript-support-plan.md +++ b/perl-typescript-support-plan.md @@ -9,7 +9,7 @@ Enable `CommunityToolkit.Aspire.Hosting.Perl` to work from a TypeScript AppHost, - `aspire docs get multi-language-integrations`: - TypeScript AppHost support comes from ATS annotations on the .NET hosting integration. - The CLI scans `[AspireExport]` methods and types, then generates the TypeScript SDK into `.modules/`. - - `Aspire.Hosting.Integration.Analyzers` is the recommended build-time guardrail for export mistakes. + - ATS diagnostics such as `ASPIREATS001` are the build-time guardrail for export mistakes. - Capability IDs must be unique; when needed, use distinct export IDs plus `MethodName` to keep the generated TypeScript names clean. - `aspire docs get multi-language-architecture`: - TypeScript AppHosts are guest processes that talk to the .NET host over local JSON-RPC. @@ -34,22 +34,22 @@ Enable `CommunityToolkit.Aspire.Hosting.Perl` to work from a TypeScript AppHost, - Existing integrations with TypeScript support follow two patterns: - Export add-methods with `[AspireExport(...)]`. - Add ATS-friendly overloads or `[AspireExportIgnore]` when the public C# overload is not suitable for polyglot callers. +- Existing ATS-enabled integrations in this repo currently rely on the diagnostics that already come with the Aspire hosting toolchain; they do not add a separate analyzer package reference in the project file. - Shared TypeScript validation already exists in `tests/CommunityToolkit.Aspire.Testing/TypeScriptAppHostTest.cs`, so Perl only needs a new test case instead of new harness code. -- `Directory.Packages.props` manages package versions centrally and does not currently include `Aspire.Hosting.Integration.Analyzers`. ## Implementation Plan 1. Enable ATS in the Perl integration project. - - Add `Aspire.Hosting.Integration.Analyzers` to `Directory.Packages.props`. - - Add a `PackageReference` for the analyzer in `src/CommunityToolkit.Aspire.Hosting.Perl/CommunityToolkit.Aspire.Hosting.Perl.csproj` with private/analyzer-only assets. - - Suppress `ASPIREATS001` in the Perl project file, matching the pattern used by other ATS-exporting integrations. + + - Align the Perl project with the current repo ATS pattern rather than introducing a standalone analyzer package. + - Suppress `ASPIREATS001` in the Perl project file, matching the pattern used by other ATS-exporting integrations. + - Rely on the existing Aspire hosting toolchain to surface ATS diagnostics during build. 2. Export the Perl surface needed by a TypeScript AppHost. - Add `[AspireExport]` to the entry points in `src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.cs`: - `AddPerlScript` - `AddPerlApi` - - `AddPerlModule` - - `AddPerlExecutable` + - Keep `AddPerlModule` and `AddPerlExecutable` out of the TypeScript surface for now because they are not part of the first validated scenario and are not yet fully implemented in C#. - Export the fluent methods required by the first sample: - `WithCpanMinus` - `WithPackage` @@ -73,7 +73,7 @@ Enable `CommunityToolkit.Aspire.Hosting.Perl` to work from a TypeScript AppHost, - `eslint.config.mjs` - `package-lock.json` if we keep parity with the committed TypeScript examples already in the repo - In `aspire.config.json`, reference the local integration project: - - `CommunityToolkit.Aspire.Hosting.Perl`: `../../../src/CommunityToolkit.Aspire.Hosting.Perl/CommunityToolkit.Aspire.Hosting.Perl.csproj` + - `CommunityToolkit.Aspire.Hosting.Perl`: `../../../../src/CommunityToolkit.Aspire.Hosting.Perl/CommunityToolkit.Aspire.Hosting.Perl.csproj` - Mirror the current C# sample behavior: - Perl API resource using `cpanm`, `Mojolicious::Lite`, `local::lib`, and an HTTP endpoint - Perl driver script with environment/reference/wait wiring @@ -132,4 +132,4 @@ Enable `CommunityToolkit.Aspire.Hosting.Perl` to work from a TypeScript AppHost, 2. Export `AddPerlApi`, `AddPerlScript`, `WithCpanMinus`, `WithPackage`, and `WithLocalLib`. 3. Generate the TypeScript example and inspect `.modules/aspire.ts`. 4. Adjust export shapes only if the analyzer or generated SDK forces it. -5. Add the TypeScript AppHost test and validate it. \ No newline at end of file +5. Add the TypeScript AppHost test and validate it. diff --git a/perl-typescript-support-roadmap.md b/perl-typescript-support-roadmap.md new file mode 100644 index 000000000..0c9ffd7e3 --- /dev/null +++ b/perl-typescript-support-roadmap.md @@ -0,0 +1,430 @@ +# Perl TypeScript Support Migration Roadmap + +This roadmap turns the existing support plan in [perl-typescript-support-plan.md](perl-typescript-support-plan.md) into reviewable execution chunks for adding TypeScript AppHost support to CommunityToolkit.Aspire.Hosting.Perl. The target is the current `examples/perl/cpanm-api-integration` scenario first, and the guiding rule is that the exported TypeScript surface should be idiomatic for TypeScript callers rather than a literal copy of every C# overload. Based on the current plan and the existing ATS patterns already used elsewhere in the repo, there is no structural reason this migration cannot be made; the main risks are export-shape compatibility, generated SDK ergonomics, and environment-level validation. + +Please as we do work on this roadmap's steps, keep the document up to date as we make changes. + +## Roadmap Chart + +| Step | Name | Small details | Blockers | Progress | +| --- | --- | --- | --- | --- | +| 1 | [Baseline and scope lock](#step-1-baseline-and-scope-lock) | Confirm the exact C# sample flow, the minimum Perl API surface for MVP, and the explicit non-goals for the first slice. | Hidden sample dependencies or fluent calls that are not yet listed in the support plan. | Complete | +| 2 | [Analyzer and project wiring](#step-2-analyzer-and-project-wiring) | Align the Perl project with the ATS warning and suppression pattern already used by ATS-enabled integrations in the repo. | Repo-specific analyzer behavior may differ from older docs that mention a standalone package. | Complete | +| 3 | [Export surface design](#step-3-export-surface-design) | Decide which APIs export cleanly as-is and which need polyglot overloads, ignores, or renamed methods. | Generic, callback-based, or parameter-ordering-hostile signatures that produce awkward TypeScript. | Complete | +| 4 | [Export add-method entry points](#step-4-export-add-method-entry-points) | Export the top-level Perl resource creation methods needed for the first scenario. | Analyzer rejects a signature or a resource type is not exportable without reshaping. | Complete | +| 5 | [Export fluent methods for the MVP chain](#step-5-export-fluent-methods-for-the-mvp-chain) | Export the fluent methods the sample actually needs, with ATS-specific overloads where needed. | Generic chain methods may need dedicated `PerlAppResource` overloads to stay natural in TypeScript. | Complete | +| 6 | [Generate and inspect the TypeScript SDK](#step-6-generate-and-inspect-the-typescript-sdk) | Restore and inspect `.modules/aspire.ts` before building the TypeScript example around it. | Generated method names, return types, or endpoint handles may not match the intended call style. | Complete | +| 7 | [Scaffold the TypeScript AppHost example](#step-7-scaffold-the-typescript-apphost-example) | Create the TypeScript AppHost project structure and wire it to the local Perl integration project. | Missing config details, package-lock policy decisions, or example-folder conventions. | Complete | +| 8 | [Port the sample behavior idiomatically](#step-8-port-the-sample-behavior-idiomatically) | Recreate the existing sample flow in `apphost.ts` using generated exports, not hand-written wrappers. | Endpoint/reference wiring may expose a rough API shape that needs a return to step 3 or 5. | Complete | +| 9 | [Add automated TypeScript AppHost validation](#step-9-add-automated-typescript-apphost-validation) | Add the Perl-specific test case that uses the shared TypeScript AppHost validation harness. | Wait targets, resource status selection, or command prerequisites may need tuning. | Complete | +| 10 | [Run end-to-end validation and capture follow-up work](#step-10-run-end-to-end-validation-and-capture-follow-up-work) | Execute the narrow and full validation passes, then record any deferred parity work. | Windows Perl environment issues, ATS edge cases, or sample-specific runtime behavior. | Complete | + +## Step 1: Baseline and scope lock + +Goal + +1. Confirm the exact behavior of the current C# sample and turn that into an agreed MVP contract for the TypeScript migration. +2. Make the cut line explicit so review can happen before any ATS annotations are added. + +Do in this chunk + +1. Read the existing C# AppHost sample and enumerate every Perl-specific call in order. +2. Confirm the first-pass exported surface includes `AddPerlApi`, `AddPerlScript`, `WithCpanMinus`, `WithPackage`, and `WithLocalLib`, plus the existing shared Aspire wiring methods already available to polyglot app hosts. +3. Record the first-pass non-goals: full Perl surface parity, Perlbrew support, Carton support, project dependency helpers, and extra example permutations. +4. Confirm what the TypeScript sample must prove: package installation, API startup, environment wiring, reference wiring, and a wait strategy that is stable for automation. + +Stop and review when + +1. There is a written MVP list that a reviewer can sign off on before code changes start. +2. Any method not needed for the first example is explicitly marked as later work instead of silently sliding into scope. + +Exit criteria + +1. The migration target is a single supported scenario with a fixed method inventory. +2. The first implementation cut line is small enough to review comfortably. + +Likely blockers + +1. The current sample may rely on behavior that is only obvious after reading helper methods or nearby resource code. +2. The first scenario may need one extra exported method that is not yet listed in the support plan. + +Completion notes + +1. The first validated scenario is locked to `examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost/AppHost.cs` with no extra Perl-specific calls beyond the ones already identified in the support plan. +2. The Perl-specific call order in the current sample is fixed as: `AddPerlApi`, `WithCpanMinus`, `WithPackage`, `WithLocalLib`, and `AddPerlScript`. +3. The current sample also depends on existing shared Aspire builder methods that should already remain outside the Perl-specific MVP export set: `WithHttpEndpoint`, `WithEnvironment`, `WithReference`, `WaitFor`, and `GetEndpoint`. +4. The MVP Perl-specific export inventory is locked to `AddPerlApi`, `AddPerlScript`, `WithCpanMinus`, `WithPackage`, and `WithLocalLib` for the first TypeScript scenario. +5. The first TypeScript example must prove package installation, Perl API startup, endpoint-to-environment wiring, resource reference wiring, and stable wait behavior for automation. +6. Deferred non-goals remain full Perl surface parity, `AddPerlModule`, `AddPerlExecutable`, Perlbrew helpers, Carton helpers, project dependency helpers, and extra sample permutations beyond `cpanm-api-integration`. +7. No additional Perl-specific blocker was found during the sample read; the remaining uncertainty is export ergonomics rather than hidden sample scope. + +[Back to roadmap chart](#roadmap-chart) + +## Step 2: Analyzer and project wiring + +Goal + +1. Turn on the ATS build-time guardrails in the same way the repo uses them for other polyglot-ready integrations. + +Do in this chunk + +1. Verify how existing ATS-enabled integrations in this repo actually get ATS diagnostics. +2. Apply the same `ASPIREATS001` suppression pattern already used by existing ATS-enabled integrations in this repo. +3. Avoid adding a standalone analyzer package unless the current repo build actually requires it. +4. Build the Perl project to confirm the ATS warnings still flow through the current Aspire hosting toolchain and the project still restores cleanly. + +Stop and review when + +1. The diff is only package-management and project-wiring work. +2. Restore and build behavior are stable before any public API annotations are touched. + +Exit criteria + +1. The Perl integration participates in ATS validation during normal build flows. +2. The Perl project matches the repo's existing ATS project configuration pattern. + +Likely blockers + +1. Older ATS documentation may mention a standalone package that is not used in this repo's current package and feed setup. +2. The Perl project may still need a small repo-specific adjustment if the current build path differs from other ATS-enabled integrations. + +Completion notes + +1. The Perl project now matches the current repo pattern by suppressing `ASPIREATS001` in the project file, consistent with other ATS-enabled integrations. +2. Existing ATS-enabled integrations in this repo do not add a standalone analyzer package reference; the current Aspire hosting toolchain already supplies the relevant ATS diagnostics. +3. Attempting to add standalone analyzer packages failed against the repo's configured feeds, so that path was rejected and removed rather than forcing broader NuGet source changes. +4. The final step 2 configuration kept the repo's existing feed mapping unchanged and aligned the Perl project to local precedent instead. +5. Perl-only validation passed after the final step 2 configuration, so the migration can proceed to export-surface design from a clean baseline. + +[Back to roadmap chart](#roadmap-chart) + +## Step 3: Export surface design + +Goal + +1. Decide the exact exported API shape before implementation so the TypeScript surface feels natural and stable. +2. Make the idiomatic-language rule explicit: keep TypeScript ergonomic even if that means not exposing every C# overload directly. + +Do in this chunk + +1. Review the Perl extension methods that are candidates for export. +2. Classify each method into one of four buckets: export as-is, export with a renamed method, export through a polyglot-specific overload, or ignore for polyglot callers. +3. Apply the same design logic used elsewhere in the repo for polyglot-friendly APIs: avoid callback-based signatures, avoid awkward optional parameter ordering, and avoid exposing types that do not translate well through ATS. +4. Decide where distinct export IDs and `MethodName` values are needed to keep generated TypeScript names clean. +5. Record any methods that should remain C#-only during the first pass. + +Stop and review when + +1. There is a simple export matrix that a reviewer can inspect before the implementation starts. +2. The team agrees that the TypeScript sample should use generated, idiomatic calls rather than force-fitting awkward C# signatures. + +Exit criteria + +1. Every MVP method has an agreed export strategy. +2. The plan is explicit about where `[AspireExportIgnore]` or internal exported overloads may be required. + +Likely blockers + +1. Generic fluent methods may technically export but still produce poor TypeScript ergonomics. +2. Endpoint-related values may need shape adjustments once the generated SDK is inspected. + +Completion notes + +1. The first-pass add-method surface is simple and ATS-friendly as written: `AddPerlApi` and `AddPerlScript` both take only the builder, a resource name, and string path values, so they do not need polyglot-only overloads for the first export pass. +2. No distinct export IDs or `MethodName` overrides are needed for the first-pass add methods because `addPerlApi` and `addPerlScript` already map cleanly to idiomatic TypeScript names. +3. `AddPerlModule` and `AddPerlExecutable` are also string-based, but they remain intentionally deferred because they are outside the validated `cpanm-api-integration` scenario and are not yet fully implemented on the C# side. +4. The MVP fluent methods under consideration remain `WithCpanMinus`, `WithPackage`, and `WithLocalLib`; each currently has scalar arguments only, so the current design assumption is that they can be tried as-is in step 5 before introducing polyglot-specific overloads. +5. `WithCarton` and `WithProjectDependencies` are deferred for first-pass scope reasons rather than ATS-shape reasons. +6. `WithPerlbrew` and `WithPerlbrewEnvironment` are deferred for the first pass because they add a second overlapping concept to the TypeScript surface and are not needed for the first validated scenario. + +Export matrix + +| Method | First-pass decision | Export strategy | Reason | +| --- | --- | --- | --- | +| `AddPerlApi` | Include now | Export as-is | String-only ATS-friendly signature; needed by the sample. | +| `AddPerlScript` | Include now | Export as-is | String-only ATS-friendly signature; needed by the sample. | +| `AddPerlModule` | Defer | None in first pass | Not needed for the MVP sample and not fully implemented in C# yet. | +| `AddPerlExecutable` | Defer | None in first pass | Not needed for the MVP sample and not fully implemented in C# yet. | +| `WithCpanMinus` | Include later in MVP | Try export as-is in step 5 | No callback or complex parameter shape. | +| `WithPackage` | Include later in MVP | Try export as-is in step 5 | Scalar parameters only; ergonomics to confirm after generation. | +| `WithLocalLib` | Include later in MVP | Try export as-is in step 5 | Scalar parameter only; ergonomics to confirm after generation. | +| `WithCarton` | Defer | None in first pass | Broader package-manager surface than the MVP needs. | +| `WithProjectDependencies` | Defer | None in first pass | Broader package-manager surface than the MVP needs. | +| `WithPerlbrew` | Defer | None in first pass | Not needed for MVP and overlaps conceptually with `WithPerlbrewEnvironment`. | +| `WithPerlbrewEnvironment` | Defer | None in first pass | Not needed for MVP and better revisited after the basic TypeScript surface is proven. | + +[Back to roadmap chart](#roadmap-chart) + +## Step 4: Export add-method entry points + +Goal + +1. Make the Perl resource creation entry points available to a TypeScript AppHost. + +Do in this chunk + +1. Add `[AspireExport]` to the agreed Perl add-methods needed for the first implementation pass. +2. Preserve the existing public C# API shape unless a polyglot-specific overload is required. +3. If the analyzer rejects a public signature, add a focused polyglot overload instead of widening the scope of the change. +4. Keep naming aligned with the generated TypeScript method style expected in the sample. + +Stop and review when + +1. Only the top-level add methods are in the diff. +2. Reviewers can confirm naming and surface area before fluent methods are added. + +Exit criteria + +1. The add-method exports compile with the analyzer enabled. +2. The exported entry points cover the first sample's resource creation needs. + +Likely blockers + +1. One of the resource creation methods may depend on a parameter or type that is not ATS-friendly. +2. Method naming may need refinement once the generated TypeScript output is visible. + +Completion notes + +1. `AddPerlScript` now exports directly as `addPerlScript` without a polyglot-specific overload. +2. `AddPerlApi` now exports directly as `addPerlApi` without a polyglot-specific overload. +3. The exported add-method entry points preserved the existing public C# signatures; no parameter reshaping or internal ATS-only overloads were needed at this stage. +4. `AddPerlModule` and `AddPerlExecutable` were intentionally left untouched in this step to preserve the first-pass scope agreed in step 3 and because they are not yet fully implemented in C#. +5. Perl-only validation passed after the add-method exports were added, so the next migration slice can move to the fluent-method exports in step 5. + +[Back to roadmap chart](#roadmap-chart) + +## Step 5: Export fluent methods for the MVP chain + +Goal + +1. Export the chainable Perl methods needed to express the first sample cleanly in TypeScript. + +Do in this chunk + +1. Add exports for `WithCpanMinus`, `WithPackage`, and `WithLocalLib`. +2. Validate whether the current generic signatures are good enough for TypeScript callers. +3. If they are not, add ATS-focused overloads on `IResourceBuilder` or another concrete resource shape so the generated API remains readable. +4. Mark raw C#-only overloads with `[AspireExportIgnore]` where the polyglot call story would otherwise be confusing. +5. Defer broader fluent parity until the first chain is working end to end. + +Stop and review when + +1. The Perl fluent chain used by the sample can be represented without obvious TypeScript friction. +2. Any extra parity work is clearly documented as deferred rather than folded into the MVP. + +Exit criteria + +1. The minimum fluent chain for the sample exists in an exportable form. +2. The code makes an intentional trade-off between C# API fidelity and TypeScript ergonomics where needed. + +Likely blockers + +1. A generic fluent helper may compile but generate an awkward or unstable TypeScript signature. +2. The best polyglot overload may need a separate method name to avoid export collisions. + +Completion notes + +1. `WithCpanMinus`, `WithPackage`, and `WithLocalLib` now export directly from the existing generic methods; no ATS-specific overloads were required for the first pass. +2. The first-pass fluent export set stayed intentionally narrow and did not widen into `WithCarton`, `WithProjectDependencies`, `WithPerlbrew`, or `WithPerlbrewEnvironment`. +3. Perl-only validation passed after the fluent exports were added, so the next check moved to generated SDK shape rather than additional compile-only changes. +4. The remaining question for these methods became purely ergonomic: whether the generated TypeScript signatures read naturally enough for the first sample chain. + +[Back to roadmap chart](#roadmap-chart) + +## Step 6: Generate and inspect the TypeScript SDK + +Goal + +1. Inspect the real generated TypeScript surface before locking in the sample implementation. + +Do in this chunk + +1. Run the generation flow that produces `.modules/aspire.ts` for the new exports. +2. Inspect method names, chaining behavior, parameter order, optional values, and endpoint/reference-related shapes. +3. Confirm the generated API reads like TypeScript and not like a mechanically translated C# API. +4. If the generated surface is awkward, return to step 3 or 5 and fix the exports before building more code on top of them. + +Stop and review when + +1. There is a concrete generated SDK surface to review. +2. The team can decide whether the API shape is good enough before the example is written. + +Exit criteria + +1. The generated SDK supports the desired sample call style. +2. Any remaining shape issues are corrected before the TypeScript example becomes the reference implementation. + +Likely blockers + +1. `withEnvironment` and endpoint handle wiring may need one more export-shape adjustment. +2. Export IDs or method names may collide or read poorly once generation happens. + +Completion notes + +1. A temporary scratch TypeScript AppHost was used to run `aspire restore` against the local Perl project without pulling step 7 into scope early. +2. The generated builder surface kept the exported add methods simple and positional: `addPerlApi(resourceName, appDirectory, scriptName)` and `addPerlScript(resourceName, appDirectory, scriptName)`. +3. The generated fluent surface is acceptable for the MVP sample: + - `withCpanMinus()` stays parameterless. + - `withPackage(packageName, { force, skipTest })` becomes a natural TypeScript call shape. + - `withLocalLib({ path })` becomes an options object with an optional `path` property. +4. The generated `PerlAppResource` surface still includes `withEnvironment`, `withReference`, `waitFor`, and `getEndpoint`, so the planned sample wiring remains viable. +5. The generated `withEnvironment` signature accepts `EndpointReference` and `Awaitable`, which means the planned `API_URL` wiring can use the Perl API endpoint directly. +6. No export-shape correction was needed after SDK inspection, so the migration can proceed to the real TypeScript AppHost scaffold in step 7. + +[Back to roadmap chart](#roadmap-chart) + +## Step 7: Scaffold the TypeScript AppHost example + +Goal + +1. Create the TypeScript AppHost project structure for the Perl sample with the minimum files needed for restore, lint, and type-checking. + +Do in this chunk + +1. Create `examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/`. +2. Add `apphost.ts`, `aspire.config.json`, `package.json`, `tsconfig.json`, and `eslint.config.mjs`. +3. Decide whether `package-lock.json` should be committed for parity with other TypeScript examples in the repo. +4. Point `aspire.config.json` at the local Perl integration project so the generated module comes from the current working tree. +5. Keep the scaffold intentionally small so reviewers can focus on structure before behavior. + +Stop and review when + +1. The example project exists and restores cleanly. +2. The folder layout and config choices match repo conventions well enough to avoid churn later. + +Exit criteria + +1. The TypeScript example can run `npm ci`, `aspire restore`, and `npx tsc --noEmit` with placeholder or near-placeholder app code. +2. The example points at the local Perl integration project rather than a published package. + +Likely blockers + +1. The repo may have an unwritten convention around lock files or ESLint config shape that needs to be copied. +2. TypeScript project structure may need a small adjustment after the first restore. + +Completion notes + +1. The new example folder `examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/` now exists with `apphost.ts`, `aspire.config.json`, `package.json`, `tsconfig.json`, and `eslint.config.mjs`. +2. `aspire.config.json` points at the local Perl hosting project with the same relative `packages` mapping pattern used by the shipped TypeScript examples in this repo. +3. `npm install` was used to generate a committed `package-lock.json`, matching the repo's existing TypeScript example pattern. +4. The scaffold validated successfully with `npm install`, `aspire restore`, and `npm run build` before any Perl-specific behavior was added. +5. The current `apphost.ts` remains intentionally minimal so the next step can focus review on the Perl sample behavior itself rather than structure and config churn. + +[Back to roadmap chart](#roadmap-chart) + +## Step 8: Port the sample behavior idiomatically + +Goal + +1. Recreate the existing Perl sample behavior in `apphost.ts` using the generated API in a way that feels natural to a TypeScript AppHost author. + +Do in this chunk + +1. Mirror the current sample's Perl API resource creation and package-install setup. +2. Add the driver script resource and the needed environment, reference, and wait wiring. +3. Keep the TypeScript code idiomatic: prefer the generated TypeScript names, avoid workarounds that leak raw C# implementation details into the example, and treat awkward generated calls as an export-design issue instead of a sample-authoring issue. +4. Verify how endpoint values should be passed into `withEnvironment` after generation. +5. Keep behavioral parity with the current C# sample while allowing TypeScript syntax and naming to look native. + +Stop and review when + +1. The example expresses the same scenario as the C# sample without obvious API awkwardness. +2. Any place where the example starts to look like a workaround is captured as an export-shape bug to fix. + +Exit criteria + +1. The TypeScript example is readable as a first-class AppHost sample. +2. The example proves the exported surface is good enough for real use, not just technically callable. + +Likely blockers + +1. Endpoint/reference values may not plug into the fluent chain exactly as expected on the first attempt. +2. A hidden assumption in the current C# sample may require one more exported helper. + +Completion notes + +1. The TypeScript AppHost now mirrors the current C# sample behavior with a Perl API resource and a Perl driver resource. +2. The Perl API uses the same first-pass behavior as the C# sample: `addPerlApi`, `withCpanMinus`, `withPackage("Mojolicious::Lite", { force: true, skipTest: true })`, `withLocalLib({ path: "local" })`, and `withHttpEndpoint({ name: "http", env: "PORT" })`. +3. The Perl driver uses `addPerlScript`, `withEnvironment("API_URL", perlApi.getEndpoint("http"))`, `withReference(perlApi)`, and `waitFor(perlApi)`. +4. The only correction needed during validation was the local project path in `aspire.config.json`; once fixed, `aspire restore` and `npm run build` both passed. +5. The generated TypeScript surface remained idiomatic enough that no additional ATS-specific overloads were required for this sample port. + +[Back to roadmap chart](#roadmap-chart) + +## Step 9: Add automated TypeScript AppHost validation + +Goal + +1. Add the Perl-specific automated validation that exercises the new TypeScript AppHost example through the shared test harness. + +Do in this chunk + +1. Add [tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs](tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs). +2. Configure the test to use `CpanmApiIntegration.AppHost.TypeScript`, `CommunityToolkit.Aspire.Hosting.Perl`, and `perl/cpanm-api-integration`. +3. Start with the expected wait target and status from the support plan, then adjust only if real execution proves they are wrong. +4. Include any required command checks such as `perl` and `cpanm`. +5. Keep this step focused on wiring the shared harness, not inventing a new validation path. + +Stop and review when + +1. The test diff is isolated and easy to evaluate. +2. The validation contract for the example is explicit enough for later maintenance. + +Exit criteria + +1. The new test compiles and exercises the shared TypeScript AppHost validation flow. +2. The expected wait resource and status are based on real sample behavior. + +Likely blockers + +1. The API resource may need a different wait target or status than the initial guess. +2. Short-lived helper resources may make the first validation setup too optimistic. + +Completion notes + +1. Added [tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs](tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs) and wired it to `CpanmApiIntegration.AppHost.TypeScript`, `CommunityToolkit.Aspire.Hosting.Perl`, and `perl/cpanm-api-integration`. +2. The validated wait contract is `waitForResources: ["perl-api"]` with `waitStatus: "up"`, plus required command checks for `perl` and `cpanm`. +3. The shared validation harness now supports a `useConfiguredPackages` mode so examples can be validated against the local package mapping already present in `aspire.config.json` when that is the intended development shape. +4. Validation exposed that the Perl TypeScript AppHost was missing the same `profiles` startup metadata used by the other working TypeScript AppHost examples; adding the `https` profile with dashboard and resource-service endpoint settings fixed the detached `aspire start` failure. +5. After those fixes, the focused Perl TypeScript AppHost test passed end to end through restore, TypeScript compilation, `aspire start`, resource wait, and `aspire describe`. + +[Back to roadmap chart](#roadmap-chart) + +## Step 10: Run end-to-end validation and capture follow-up work + +Goal + +1. Prove the full path works and document what remains after the MVP is green. + +Do in this chunk + +1. Run the narrow build and test checks for the touched code. +2. Run the Perl test project and the new TypeScript AppHost test. +3. In the example folder, run `npm ci`, `aspire restore`, inspect `.modules/aspire.ts`, and run `npx tsc --noEmit`. +4. Record the actual results in this roadmap, including any export-shape adjustments or environment-specific issues discovered during validation. +5. Capture the deferred follow-up list: full fluent parity, Perlbrew support, Carton, project dependency helpers, and any Windows-specific environment caveats. + +Stop and review when + +1. There is a complete validation record for the MVP slice. +2. Follow-up work is separated cleanly from the first successful migration. + +Exit criteria + +1. The first TypeScript AppHost Perl scenario is green end to end, or the remaining blockers are concrete and narrowly scoped. +2. The roadmap reflects what was actually learned, not just the original guess. + +Likely blockers + +1. Environment-specific behavior on Windows may require a documented caveat even if the export work is correct. +2. One remaining ATS issue may only become visible after the full example and test flow run together. + +Completion notes + +1. End-to-end example validation passed in the TypeScript AppHost folder with `npm ci`, `aspire restore --apphost apphost.ts --non-interactive`, and `npx tsc --noEmit`. +2. The generated SDK in `.modules/aspire.ts` now confirms the intended Perl-specific surface exactly: `addPerlApi`, `addPerlScript`, `withCpanMinus`, `withPackage`, and `withLocalLib` are present; `addPerlModule` and `addPerlExecutable` are intentionally absent. +3. The missing `addPerlModule` and `addPerlExecutable` methods are not a regression in ATS export wiring; they remain deferred because those entry points are not fully implemented on the C# side and are outside the first validated scenario. +4. The focused TypeScript AppHost validation test passed, and the full Perl test project also passed with 148 of 148 tests green. +5. The remaining deferred follow-up list is broader parity work rather than MVP breakage: Carton helpers, project dependency helpers, Perlbrew-related surface, and any later revisit of module/executable support after the underlying C# implementation is finished. + +[Back to roadmap chart](#roadmap-chart) diff --git a/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.PackageManager.cs b/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.PackageManager.cs index 637786c8e..1c02fbc18 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.PackageManager.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.PackageManager.cs @@ -21,7 +21,7 @@ public static partial class PerlAppResourceBuilderExtensions /// The type of the Perl application resource. /// The resource builder. /// A reference to the . - [AspireExport] + [AspireExport("withCpanMinus", Description = "Configures the Perl resource to use cpanm for package installation")] public static IResourceBuilder WithCpanMinus( this IResourceBuilder builder) where TResource : PerlAppResource { @@ -212,7 +212,7 @@ public static IResourceBuilder WithCarton( /// cpan by default, or cpanm if /// was called. /// - [AspireExport] + [AspireExport("withPackage", Description = "Installs a Perl package before the resource starts")] public static IResourceBuilder WithPackage( this IResourceBuilder builder, string packageName, diff --git a/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.cs index e38740f71..a13f91806 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.cs @@ -49,7 +49,7 @@ public static partial class PerlAppResourceBuilderExtensions /// builder.Build().Run(); /// /// - [AspireExport] + [AspireExport("addPerlScript", Description = "Adds a Perl script resource")] public static IResourceBuilder AddPerlScript( this IDistributedApplicationBuilder builder, [ResourceName] string resourceName, string appDirectory, string scriptName) => AddPerlAppCore(builder, resourceName, appDirectory, EntrypointType.Script, scriptName, DefaultPerlEnvironment); @@ -89,7 +89,7 @@ public static IResourceBuilder AddPerlScript( /// builder.Build().Run(); /// /// - [AspireExport] + [AspireExport("addPerlApi", Description = "Adds a Perl API resource")] public static IResourceBuilder AddPerlApi( this IDistributedApplicationBuilder builder, [ResourceName] string resourceName, string appDirectory, string scriptName) => AddPerlAppCore(builder, resourceName, appDirectory, EntrypointType.API, scriptName, DefaultPerlEnvironment, "daemon"); @@ -466,7 +466,7 @@ await interactionService.PromptNotificationAsync( /// While it is possible to use cpan with Local::Lib it is complicated to model in Aspire. /// /// - [AspireExport] + [AspireExport("withLocalLib", Description = "Configures a local::lib directory for Perl module isolation")] public static IResourceBuilder WithLocalLib( this IResourceBuilder builder, string path = "local") where TResource : PerlAppResource diff --git a/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs new file mode 100644 index 000000000..6002ff822 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs @@ -0,0 +1,20 @@ +using CommunityToolkit.Aspire.Testing; + +namespace CommunityToolkit.Aspire.Hosting.Perl.Tests; + +public class TypeScriptAppHostTests +{ + [Fact] + public async Task TypeScriptAppHostCompilesAndStarts() + { + await TypeScriptAppHostTest.Run( + appHostProject: "CpanmApiIntegration.AppHost.TypeScript", + packageName: "CommunityToolkit.Aspire.Hosting.Perl", + exampleName: "perl/cpanm-api-integration", + waitForResources: ["perl-api"], + waitStatus: "up", + requiredCommands: ["perl", "cpanm"], + useConfiguredPackages: true, + cancellationToken: TestContext.Current.CancellationToken); + } +} \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Testing/TypeScriptAppHostTest.cs b/tests/CommunityToolkit.Aspire.Testing/TypeScriptAppHostTest.cs index adcb7a39c..6cf1cfb30 100644 --- a/tests/CommunityToolkit.Aspire.Testing/TypeScriptAppHostTest.cs +++ b/tests/CommunityToolkit.Aspire.Testing/TypeScriptAppHostTest.cs @@ -14,6 +14,7 @@ public static class TypeScriptAppHostTest /// The resources that must reach the expected Aspire state, if any. /// The Aspire resource status to wait for. /// Optional commands that must exist on PATH before validation runs. + /// to validate the AppHost using the package mappings already present in aspire.config.json instead of packing a local polyglot package first. /// Optional dictionary of secret key-value pairs to set via aspire secret set before starting the app host. /// The cancellation token. public static async Task Run( @@ -23,6 +24,7 @@ public static async Task Run( IEnumerable waitForResources, string waitStatus = "healthy", IEnumerable? requiredCommands = null, + bool useConfiguredPackages = false, Dictionary? secrets = null, CancellationToken cancellationToken = default) { @@ -75,6 +77,11 @@ public static async Task Run( arguments.Add(string.Join(',', commands)); } + if (useConfiguredPackages) + { + arguments.Add("-UseConfiguredPackages"); + } + if (secrets is { Count: > 0 }) { arguments.Add("-Secrets"); From 73790135a8236eed5bd23d326857126e16fbd913 Mon Sep 17 00:00:00 2001 From: Matthew Austew Date: Mon, 25 May 2026 10:40:46 -0500 Subject: [PATCH 03/11] Roadmap pushed up for final feature expansion. --- perl-typescript-support-roadmap.md | 313 +++++++++++++++++++++++++++++ 1 file changed, 313 insertions(+) diff --git a/perl-typescript-support-roadmap.md b/perl-typescript-support-roadmap.md index 0c9ffd7e3..d07461343 100644 --- a/perl-typescript-support-roadmap.md +++ b/perl-typescript-support-roadmap.md @@ -18,6 +18,16 @@ Please as we do work on this roadmap's steps, keep the document up to date as we | 8 | [Port the sample behavior idiomatically](#step-8-port-the-sample-behavior-idiomatically) | Recreate the existing sample flow in `apphost.ts` using generated exports, not hand-written wrappers. | Endpoint/reference wiring may expose a rough API shape that needs a return to step 3 or 5. | Complete | | 9 | [Add automated TypeScript AppHost validation](#step-9-add-automated-typescript-apphost-validation) | Add the Perl-specific test case that uses the shared TypeScript AppHost validation harness. | Wait targets, resource status selection, or command prerequisites may need tuning. | Complete | | 10 | [Run end-to-end validation and capture follow-up work](#step-10-run-end-to-end-validation-and-capture-follow-up-work) | Execute the narrow and full validation passes, then record any deferred parity work. | Windows Perl environment issues, ATS edge cases, or sample-specific runtime behavior. | Complete | +| 11 | [Lock the full-capability finish line](#step-11-lock-the-full-capability-finish-line) | Define exactly what counts toward 100% TypeScript capability coverage and turn the remaining public Perl APIs into a tracked matrix. | Disagreement over whether experimental or partially implemented C# APIs count toward the finish line. | Not started | +| 12 | [Export package-manager parity methods](#step-12-export-package-manager-parity-methods) | Export `WithCarton` and `WithProjectDependencies` and confirm their generated TypeScript shapes are acceptable. | `cartonDeployment` option shape or Carton/WithPackage interaction may need polyglot-specific handling. | Not started | +| 13 | [Validate project-dependency scenarios](#step-13-validate-project-dependency-scenarios) | Add TypeScript AppHost examples and tests for `cpanm --installdeps` and Carton-based dependency flows. | Missing `cpanfile` fixtures, `cpanfile.snapshot` handling, or environment prerequisites may make the first scenario too optimistic. | Not started | +| 14 | [Design and export the Perlbrew surface](#step-14-design-and-export-the-perlbrew-surface) | Decide whether TypeScript should expose `WithPerlbrew`, `WithPerlbrewEnvironment`, or a single canonical method, then export it. | Alias duplication and Windows-specific failure semantics may make the raw C# surface too awkward for polyglot callers. | Not started | +| 15 | [Validate Perlbrew across platforms](#step-15-validate-perlbrew-across-platforms) | Add Linux-positive TypeScript validation and explicit Windows behavior checks for the Perlbrew flow. | Perlbrew is Linux-only, so the validation matrix likely needs platform-aware test gating or a Linux-only lane. | Not started | +| 16 | [Export the certificate-trust capability](#step-16-export-the-certificate-trust-capability) | Export `WithPerlCertificateTrust` intentionally and decide how experimental API status should surface in the TypeScript story. | Experimental API policy or ATS handling may require an explicit support decision before export. | Not started | +| 17 | [Validate certificate-trust behavior](#step-17-validate-certificate-trust-behavior) | Add a TypeScript scenario that proves certificate bundle propagation to both runtime and installer paths. | A realistic HTTPS fixture and cross-platform certificate handling may be harder than the export itself. | Not started | +| 18 | [Finish module and executable support in C#](#step-18-finish-module-and-executable-support-in-c) | Close the underlying C# behavior gaps for `AddPerlModule` and `AddPerlExecutable` before exporting them. | The methods exist today, but runtime, publish, or sample-level behavior may still be under-specified for full support. | Not started | +| 19 | [Export and validate module and executable entry points](#step-19-export-and-validate-module-and-executable-entry-points) | Export `AddPerlModule` and `AddPerlExecutable` only after the C# implementation is promoted to supported behavior. | The generated SDK can be added quickly, but example and runtime proof may lag behind implementation work. | Not started | +| 20 | [Close the parity gap](#step-20-close-the-parity-gap) | Run the final parity audit, add any missing test lanes, and update docs so the supported TypeScript surface is unambiguous. | CI coverage or platform-specific prerequisites may still leave one capability unverified unless the matrix is widened. | Not started | ## Step 1: Baseline and scope lock @@ -428,3 +438,306 @@ Completion notes 5. The remaining deferred follow-up list is broader parity work rather than MVP breakage: Carton helpers, project dependency helpers, Perlbrew-related surface, and any later revisit of module/executable support after the underlying C# implementation is finished. [Back to roadmap chart](#roadmap-chart) + +## Step 11: Lock the full-capability finish line + +Goal + +1. Define what 100% TypeScript capability coverage means for the Perl integration so the remaining work can be measured cleanly. +2. Turn the remaining public Perl AppHost APIs into a tracked matrix with an explicit status for each capability. + +Do in this chunk + +1. Inventory every remaining public Perl AppHost-facing API that is not yet available from the generated TypeScript SDK. +2. Split the inventory into three buckets: ready to export now, blocked on a C# implementation gap, and experimental or policy-dependent. +3. Decide whether alias-style APIs such as `WithPerlbrew` should be exported directly, ignored, or collapsed into a single canonical TypeScript method. +4. Decide whether experimental APIs such as `WithPerlCertificateTrust` count toward the 100% parity finish line now or only after an explicit support decision. +5. Record the validation expectation for each capability: generated SDK presence, compile-time usage, runtime scenario, and platform-specific negative behavior where relevant. + +Stop and review when + +1. There is a single finish-line matrix that says exactly what work remains. +2. Reviewers can see which gaps are export-only versus blocked on deeper C# support. + +Exit criteria + +1. The roadmap has a precise remaining capability list instead of a generic parity aspiration. +2. Every later step can point back to a specific capability bucket in this matrix. + +Likely blockers + +1. The team may need to decide whether experimental and partially implemented APIs count toward “100%” immediately. +2. One alias or convenience method may be intentionally C#-only even if the underlying behavior is supported. + +[Back to roadmap chart](#roadmap-chart) + +## Step 12: Export package-manager parity methods + +Goal + +1. Expose the remaining package-manager capabilities needed for TypeScript parity with the supported Perl package flows. + +Do in this chunk + +1. Add TypeScript exports for `WithCarton` and `WithProjectDependencies`. +2. Inspect the generated SDK shape for `cartonDeployment` and confirm it reads naturally for TypeScript callers. +3. Keep the C# interaction rules intact, especially the existing `WithPackage()` and `WithCarton()` incompatibility. +4. Add ATS-specific overloads or ignore wrappers only if the generated TypeScript API is awkward or misleading. + +Stop and review when + +1. The generated TypeScript package-manager surface is readable without explaining C# implementation details. +2. Negative interaction rules remain explicit rather than hidden in runtime exceptions alone. + +Exit criteria + +1. `WithCarton` and `WithProjectDependencies` appear in `.modules/aspire.ts` with acceptable TypeScript call shapes. +2. The export diff is limited to package-manager parity and does not widen into unrelated capability work. + +Likely blockers + +1. `WithProjectDependencies(cartonDeployment: true)` may generate an options shape that needs a TypeScript-specific overload. +2. The Carton and per-package installer interaction may need clearer guidance in generated or human-facing docs. + +[Back to roadmap chart](#roadmap-chart) + +## Step 13: Validate project-dependency scenarios + +Goal + +1. Prove the exported package-manager parity surface works in real TypeScript AppHost scenarios, not just in generated type signatures. + +Do in this chunk + +1. Add a TypeScript AppHost scenario for `WithProjectDependencies()` in the cpanm flow. +2. Add a Carton-focused TypeScript AppHost scenario that exercises `.WithCarton().WithProjectDependencies()` with the right `cpanfile` expectations. +3. Add shared-harness tests for both scenarios and keep the wait strategy stable for automation. +4. Cover the `cpanfile.snapshot` requirement and any command prerequisites explicitly in the tests. + +Stop and review when + +1. There is at least one runtime-backed TypeScript AppHost test for each supported project-dependency path. +2. Reviewers can see the difference between type-only coverage and behavior coverage. + +Exit criteria + +1. The TypeScript AppHost test suite proves both cpanm-based and Carton-based project dependency flows. +2. Any missing runtime prerequisite is captured as a concrete blocker rather than a vague parity gap. + +Likely blockers + +1. The Carton scenario may need new example fixtures such as `cpanfile` and `cpanfile.snapshot`. +2. Command availability such as `carton` may require new required-command gating in the test matrix. + +[Back to roadmap chart](#roadmap-chart) + +## Step 14: Design and export the Perlbrew surface + +Goal + +1. Make the Perlbrew TypeScript story intentional instead of blindly mirroring two overlapping C# methods. + +Do in this chunk + +1. Decide whether TypeScript should expose both `WithPerlbrew` and `WithPerlbrewEnvironment`, or only one canonical method. +2. Add exports for the chosen Perlbrew surface, reshaping arguments only if that materially improves TypeScript readability. +3. Preserve the current Windows failure behavior and Linux-only support constraints. +4. Confirm the generated method names make the Perlbrew flow understandable without leaking internal C# naming history. + +Stop and review when + +1. The Perlbrew TypeScript API does not present duplicate or confusing entry points. +2. The support decision is explicit enough that later docs and tests can follow it consistently. + +Exit criteria + +1. The Perlbrew capability appears in the generated SDK with a deliberate TypeScript shape. +2. The roadmap records whether alias-style C# methods remain C#-only or are intentionally exported. + +Likely blockers + +1. Exporting both Perlbrew methods may create avoidable duplication in the TypeScript surface. +2. Windows-specific failure messaging may complicate what “supported in TypeScript” means for this capability. + +[Back to roadmap chart](#roadmap-chart) + +## Step 15: Validate Perlbrew across platforms + +Goal + +1. Prove the Perlbrew TypeScript story works where supported and fails predictably where unsupported. + +Do in this chunk + +1. Add a Linux-positive TypeScript AppHost scenario that exercises the Perlbrew flow end to end. +2. Add explicit coverage for the current Windows path so the expected failure behavior is documented and tested. +3. Extend the shared validation harness or test conventions if platform gating is required. +4. Record any Linux-only prerequisites or CI-lane needs rather than assuming the existing Windows flow is enough. + +Stop and review when + +1. Platform behavior is explicit and repeatable. +2. Reviewers can see what is genuinely supported on Linux and what is intentionally unsupported on Windows. + +Exit criteria + +1. The Perlbrew TypeScript capability has both positive and negative validation where appropriate. +2. Platform-specific requirements are captured in tests or docs rather than tribal knowledge. + +Likely blockers + +1. Perlbrew is Linux-only, so an additional test lane or devcontainer validation path may be required. +2. The existing shared harness may need a small extension to express OS-specific expectations cleanly. + +[Back to roadmap chart](#roadmap-chart) + +## Step 16: Export the certificate-trust capability + +Goal + +1. Decide and implement how `WithPerlCertificateTrust` should participate in the TypeScript surface. + +Do in this chunk + +1. Confirm whether the experimental certificate-trust API belongs in the 100% parity target now. +2. If yes, add the export and inspect the generated TypeScript shape. +3. Keep the current installer-propagation and idempotency behavior intact. +4. If the experimental status needs special treatment, document that policy rather than silently deferring the method. + +Stop and review when + +1. There is a clear support decision for the certificate-trust capability. +2. The generated TypeScript API does not hide that the method is still experimental if that distinction still matters. + +Exit criteria + +1. `WithPerlCertificateTrust` is either intentionally exported with a validation plan or intentionally excluded with an explicit rationale. +2. The roadmap no longer treats certificate trust as an unnamed future item. + +Likely blockers + +1. Experimental API policy may require a broader repo-level decision. +2. The export itself is easy, but end-to-end validation may require more infrastructure than the method shape suggests. + +[Back to roadmap chart](#roadmap-chart) + +## Step 17: Validate certificate-trust behavior + +Goal + +1. Prove that TypeScript callers can rely on certificate bundle propagation for both runtime and installer flows. + +Do in this chunk + +1. Add a TypeScript scenario that exercises an HTTPS or CA-bundle-dependent flow. +2. Validate that runtime resources receive the expected certificate-related environment variables. +3. Validate that installer child resources also receive the propagated certificate trust settings. +4. Keep the scenario narrow enough that failures identify trust propagation problems rather than unrelated network complexity. + +Stop and review when + +1. The certificate-trust capability is backed by a concrete behavior test. +2. The scenario is simple enough that future regressions are diagnosable. + +Exit criteria + +1. The TypeScript AppHost suite contains at least one end-to-end certificate-trust proof. +2. Any platform-specific caveat is documented directly in the roadmap. + +Likely blockers + +1. Creating a realistic HTTPS or custom-CA test fixture may take more setup than the export work itself. +2. Cross-platform certificate bundle handling may require separate validation assumptions for Windows and Linux. + +[Back to roadmap chart](#roadmap-chart) + +## Step 18: Finish module and executable support in C# + +Goal + +1. Promote `AddPerlModule` and `AddPerlExecutable` from “builder exists” to “behavior is fully supported” before exposing them in TypeScript. + +Do in this chunk + +1. Identify the exact implementation gaps that currently make module and executable support feel incomplete. +2. Add or strengthen runtime, publish-mode, and integration coverage for those entry points on the C# side. +3. Decide whether both methods truly belong in the long-term supported surface or whether one should remain internal or explicitly unsupported. +4. Avoid exporting them to TypeScript until the C# story is no longer caveated. + +Stop and review when + +1. The underlying C# support decision is settled. +2. Reviewers can see that TypeScript export is no longer leading the C# implementation. + +Exit criteria + +1. `AddPerlModule` and `AddPerlExecutable` are either promoted to fully supported behavior or explicitly carved out of the parity target with written justification. +2. The roadmap no longer relies on vague “not fully implemented” language. + +Likely blockers + +1. The current behavior may be adequate for unit tests but not for realistic runtime or publish scenarios. +2. The supporting examples and validation fixtures may not exist yet. + +[Back to roadmap chart](#roadmap-chart) + +## Step 19: Export and validate module and executable entry points + +Goal + +1. Expose module and executable entry points to TypeScript only after step 18 confirms they are genuinely supported. + +Do in this chunk + +1. Add exports for `AddPerlModule` and `AddPerlExecutable` once their C# behavior is approved. +2. Inspect the generated TypeScript SDK to ensure the entry points read naturally and do not need polyglot-only reshaping. +3. Add at least one TypeScript AppHost scenario for the module flow and one for the executable flow. +4. Add automated validation that proves those entry points compile and run rather than merely appearing in `.modules/aspire.ts`. + +Stop and review when + +1. The generated SDK and the example behavior agree on what “supported” means. +2. Reviewers can validate both new entry points independently. + +Exit criteria + +1. `addPerlModule` and `addPerlExecutable` are present in the generated SDK only after their runtime behavior is proven. +2. The final TypeScript capability matrix includes behavior coverage for these entry points, not just export coverage. + +Likely blockers + +1. Example fixtures for module and executable flows may need to be created from scratch. +2. Publish-mode or deployment assumptions may still diverge from simple local run-mode validation. + +[Back to roadmap chart](#roadmap-chart) + +## Step 20: Close the parity gap + +Goal + +1. Finish the TypeScript parity work with a documented, test-backed answer to “what is fully covered now?” + +Do in this chunk + +1. Generate a final capability matrix that compares the intended supported C# Perl AppHost APIs to the generated TypeScript SDK surface. +2. Run the full Perl test project and the expanded TypeScript AppHost validation set. +3. Add any missing CI or platform-specific validation lane needed to keep the new capabilities from regressing. +4. Update the roadmap, support plan, README, and any sample docs so the supported TypeScript surface is unambiguous. +5. Record the residual exclusions, if any, as explicit product decisions rather than undocumented omissions. + +Stop and review when + +1. The supported TypeScript surface can be described in one precise list. +2. There is no remaining disagreement over whether the roadmap is complete. + +Exit criteria + +1. Every supported Perl TypeScript capability has generated-SDK coverage and the right level of behavior validation. +2. The final documentation matches the actual shipped TypeScript surface and the CI matrix can keep it that way. + +Likely blockers + +1. Linux-only or HTTPS-specific capabilities may require extra CI wiring before the parity claim is fully defensible. +2. One capability may still need a product decision even after the technical work is understood. + +[Back to roadmap chart](#roadmap-chart) From 3dfa92107c05a0613d04bff7365d1fb67a8c4f18 Mon Sep 17 00:00:00 2001 From: Omnideth Date: Mon, 25 May 2026 15:42:13 -0500 Subject: [PATCH 04/11] Committing these to pivot boxes, this is steps 11-13, I do have a question about if these validate-typescript-apphost.ps1 changes are required. Will interrogate later. --- eng/testing/validate-typescript-apphost.ps1 | 127 +++++++++++++++++- .../apphost.ts | 16 +-- .../aspire.config.json | 2 +- .../package-lock.json | 5 - examples/perl/cpanm-api-integration/README.md | 21 --- .../cpanm-api-integration/scripts/cpanfile | 1 + perl-typescript-support-roadmap.md | 74 +++++++--- ...esourceBuilderExtensions.PackageManager.cs | 8 ++ .../TypeScriptAppHostTests.cs | 10 +- .../TypeScriptAppHostTest.cs | 39 ++++++ 10 files changed, 246 insertions(+), 57 deletions(-) delete mode 100644 examples/perl/cpanm-api-integration/README.md create mode 100644 examples/perl/cpanm-api-integration/scripts/cpanfile diff --git a/eng/testing/validate-typescript-apphost.ps1 b/eng/testing/validate-typescript-apphost.ps1 index 1db17b9b9..94cf5e02a 100644 --- a/eng/testing/validate-typescript-apphost.ps1 +++ b/eng/testing/validate-typescript-apphost.ps1 @@ -22,6 +22,15 @@ param( [int]$WaitTimeoutSeconds = 180, + [string]$HttpProbeResource = "", + + [string]$HttpProbeEndpointName = "http", + + [string]$HttpProbePath = "", + + [AllowEmptyString()] + [string]$HttpProbeExpectedText, + [string[]]$Secrets = @() ) @@ -69,6 +78,26 @@ function Invoke-ExternalCommand { } } +function Invoke-ExternalCommandCaptureOutput { + param( + [Parameter(Mandatory = $true)] + [string]$FilePath, + + [Parameter(Mandatory = $true)] + [string[]]$Arguments + ) + + $resolvedFilePath = Resolve-ExternalCommandPath $FilePath + $output = & $resolvedFilePath @Arguments 2>&1 + + if ($LASTEXITCODE -ne 0) { + $joinedArguments = [string]::Join(" ", $Arguments) + throw "Command failed with exit code ${LASTEXITCODE}: $FilePath $joinedArguments" + } + + return (($output | ForEach-Object { $_.ToString() }) -join [Environment]::NewLine) +} + function Invoke-CleanupStep { param( [Parameter(Mandatory = $true)] @@ -128,6 +157,78 @@ function Remove-PathWithRetry { } } +function ConvertFrom-AspireJsonOutput { + param( + [Parameter(Mandatory = $true)] + [string]$Text + ) + + $jsonStartIndex = $Text.IndexOf('{') + $jsonEndIndex = $Text.LastIndexOf('}') + + if ($jsonStartIndex -lt 0 -or $jsonEndIndex -lt $jsonStartIndex) { + throw "Could not find a JSON payload in Aspire command output." + } + + $jsonText = $Text.Substring($jsonStartIndex, ($jsonEndIndex - $jsonStartIndex) + 1) + return $jsonText | ConvertFrom-Json -AsHashtable -Depth 100 +} + +function Get-ResourceEndpointUrl { + param( + [Parameter(Mandatory = $true)] + [hashtable]$DescribePayload, + + [Parameter(Mandatory = $true)] + [string]$ResourceName, + + [Parameter(Mandatory = $true)] + [string]$EndpointName + ) + + $resource = @($DescribePayload["resources"]) | + Where-Object { + $_["displayName"] -eq $ResourceName -or $_["name"] -eq $ResourceName + } | + Select-Object -First 1 + + if ($null -eq $resource) { + throw "Resource '$ResourceName' was not present in 'aspire describe' output." + } + + $endpoint = @($resource["urls"]) | + Where-Object { $_["name"] -eq $EndpointName } | + Select-Object -First 1 + + if ($null -eq $endpoint -or [string]::IsNullOrWhiteSpace($endpoint["url"])) { + throw "Resource '$ResourceName' did not expose endpoint '$EndpointName' in 'aspire describe' output." + } + + return $endpoint["url"] +} + +function Assert-HttpProbeResponse { + param( + [Parameter(Mandatory = $true)] + [string]$BaseUrl, + + [Parameter(Mandatory = $true)] + [string]$Path, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [string]$ExpectedText + ) + + $probeUri = [System.Uri]::new([System.Uri]$BaseUrl, $Path) + $response = Invoke-WebRequest -Uri $probeUri -TimeoutSec 30 + $actualText = $response.Content + + if ($actualText -ne $ExpectedText) { + throw "HTTP probe for '$probeUri' returned '$actualText' instead of expected '$ExpectedText'." + } +} + $resolvedAppHostPath = (Resolve-Path $AppHostPath).Path $resolvedPackageProjectPath = $null $localSource = $null @@ -229,6 +330,22 @@ foreach ($secret in $Secrets) { $parsedSecrets.Add(@($key, $value)) } +$hasHttpProbe = + -not [string]::IsNullOrWhiteSpace($HttpProbeResource) -or + -not [string]::IsNullOrWhiteSpace($HttpProbePath) -or + $PSBoundParameters.ContainsKey("HttpProbeExpectedText") + +if ($hasHttpProbe -and ( + [string]::IsNullOrWhiteSpace($HttpProbeResource) -or + [string]::IsNullOrWhiteSpace($HttpProbePath) -or + -not $PSBoundParameters.ContainsKey("HttpProbeExpectedText"))) { + throw "HttpProbeResource, HttpProbePath, and HttpProbeExpectedText must all be provided together." +} + +if ($hasHttpProbe -and [string]::IsNullOrWhiteSpace($HttpProbeEndpointName)) { + throw "HttpProbeEndpointName cannot be empty when HTTP probing is enabled." +} + try { $originalConfig = Get-Content -Path $configPath -Raw if (-not $UseConfiguredPackages) { @@ -338,12 +455,20 @@ try { ) } - Invoke-ExternalCommand "aspire" @( + $describeOutput = Invoke-ExternalCommandCaptureOutput "aspire" @( "describe", "--apphost", $resolvedAppHostPath, "--format", "Json", "--log-level", "debug" ) + + Write-Output $describeOutput + + if ($hasHttpProbe) { + $describePayload = ConvertFrom-AspireJsonOutput $describeOutput + $endpointUrl = Get-ResourceEndpointUrl -DescribePayload $describePayload -ResourceName $HttpProbeResource -EndpointName $HttpProbeEndpointName + Assert-HttpProbeResponse -BaseUrl $endpointUrl -Path $HttpProbePath -ExpectedText $HttpProbeExpectedText + } } finally { Pop-Location diff --git a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/apphost.ts b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/apphost.ts index 251fd1ed9..4be032ded 100644 --- a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/apphost.ts +++ b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/apphost.ts @@ -2,15 +2,15 @@ import { createBuilder } from "./.modules/aspire.js"; const builder = await createBuilder(); -const perlApi = await builder.addPerlApi("perl-api", ".", "../scripts/API.pl"); -await perlApi.withCpanMinus(); -await perlApi.withPackage("Mojolicious::Lite", { force: true, skipTest: true }); -await perlApi.withLocalLib({ path: "local" }); -await perlApi.withHttpEndpoint({ name: "http", env: "PORT" }); +const cartonProjectApi = await builder.addPerlApi("perl-api", "../scripts", "API.pl"); +await cartonProjectApi.withCarton(); +await cartonProjectApi.withProjectDependencies({ cartonDeployment: true }); +await cartonProjectApi.withLocalLib({ path: "local" }); +await cartonProjectApi.withHttpEndpoint({ name: "http", env: "PORT" }); const perlDriver = await builder.addPerlScript("perl-driver", "../scripts", "driver.pl"); -await perlDriver.withEnvironment("API_URL", perlApi.getEndpoint("http")); -await perlDriver.withReference(perlApi); -await perlDriver.waitFor(perlApi); +await perlDriver.withEnvironment("API_URL", cartonProjectApi.getEndpoint("http")); +await perlDriver.withReference(cartonProjectApi); +await perlDriver.waitFor(cartonProjectApi); await builder.build().run(); \ No newline at end of file diff --git a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/aspire.config.json b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/aspire.config.json index d6aeb73a6..e402cd952 100644 --- a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/aspire.config.json +++ b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/aspire.config.json @@ -6,6 +6,7 @@ "sdk": { "version": "13.3.0" }, + "channel": "stable", "profiles": { "https": { "applicationUrl": "https://localhost:17453;http://localhost:15453", @@ -15,7 +16,6 @@ } } }, - "channel": "stable", "packages": { "CommunityToolkit.Aspire.Hosting.Perl": "../../../../src/CommunityToolkit.Aspire.Hosting.Perl/CommunityToolkit.Aspire.Hosting.Perl.csproj" } diff --git a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/package-lock.json b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/package-lock.json index b54a33af7..5691f71de 100644 --- a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/package-lock.json +++ b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/package-lock.json @@ -713,7 +713,6 @@ "integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.59.4", "@typescript-eslint/types": "8.59.4", @@ -905,7 +904,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1142,7 +1140,6 @@ "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -1848,7 +1845,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1930,7 +1926,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/examples/perl/cpanm-api-integration/README.md b/examples/perl/cpanm-api-integration/README.md deleted file mode 100644 index 4875c957f..000000000 --- a/examples/perl/cpanm-api-integration/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# cpanm-api-integration - -Minimal Perl API example for integration tests using `cpanm` and `Mojolicious::Lite`. - -## Project -- `CpanmApiIntegration.AppHost` (Aspire AppHost) - -## Perl focus -- Uses `AddPerlApi(...)` -- Uses `WithCpanMinus()` -- Uses `WithPackage("Mojolicious::Lite")` -- Uses HTTP endpoint wiring with `PORT` -- No HTTPS and no OpenTelemetry setup - -## Endpoint -- `/` - -## Run -```powershell -aspire run -``` diff --git a/examples/perl/cpanm-api-integration/scripts/cpanfile b/examples/perl/cpanm-api-integration/scripts/cpanfile new file mode 100644 index 000000000..f03bdf1a5 --- /dev/null +++ b/examples/perl/cpanm-api-integration/scripts/cpanfile @@ -0,0 +1 @@ +requires 'Mojolicious::Lite'; \ No newline at end of file diff --git a/perl-typescript-support-roadmap.md b/perl-typescript-support-roadmap.md index d07461343..7afbe0f4c 100644 --- a/perl-typescript-support-roadmap.md +++ b/perl-typescript-support-roadmap.md @@ -1,6 +1,6 @@ # Perl TypeScript Support Migration Roadmap -This roadmap turns the existing support plan in [perl-typescript-support-plan.md](perl-typescript-support-plan.md) into reviewable execution chunks for adding TypeScript AppHost support to CommunityToolkit.Aspire.Hosting.Perl. The target is the current `examples/perl/cpanm-api-integration` scenario first, and the guiding rule is that the exported TypeScript surface should be idiomatic for TypeScript callers rather than a literal copy of every C# overload. Based on the current plan and the existing ATS patterns already used elsewhere in the repo, there is no structural reason this migration cannot be made; the main risks are export-shape compatibility, generated SDK ergonomics, and environment-level validation. +This roadmap turns the existing support plan in [perl-typescript-support-plan.md](perl-typescript-support-plan.md) into reviewable execution chunks for adding TypeScript AppHost support to CommunityToolkit.Aspire.Hosting.Perl. The target started with the current `examples/perl/cpanm-api-integration` scenario first, and the finish line is now the full public Perl AppHost-facing C# surface: every public API should be exported to TypeScript, including experimental APIs, while still preferring TypeScript-friendly shapes where overload translation is needed. Based on the current plan and the existing ATS patterns already used elsewhere in the repo, there is no structural reason this migration cannot be made; the main risks are export-shape compatibility, generated SDK ergonomics, and environment-level validation. Please as we do work on this roadmap's steps, keep the document up to date as we make changes. @@ -18,12 +18,12 @@ Please as we do work on this roadmap's steps, keep the document up to date as we | 8 | [Port the sample behavior idiomatically](#step-8-port-the-sample-behavior-idiomatically) | Recreate the existing sample flow in `apphost.ts` using generated exports, not hand-written wrappers. | Endpoint/reference wiring may expose a rough API shape that needs a return to step 3 or 5. | Complete | | 9 | [Add automated TypeScript AppHost validation](#step-9-add-automated-typescript-apphost-validation) | Add the Perl-specific test case that uses the shared TypeScript AppHost validation harness. | Wait targets, resource status selection, or command prerequisites may need tuning. | Complete | | 10 | [Run end-to-end validation and capture follow-up work](#step-10-run-end-to-end-validation-and-capture-follow-up-work) | Execute the narrow and full validation passes, then record any deferred parity work. | Windows Perl environment issues, ATS edge cases, or sample-specific runtime behavior. | Complete | -| 11 | [Lock the full-capability finish line](#step-11-lock-the-full-capability-finish-line) | Define exactly what counts toward 100% TypeScript capability coverage and turn the remaining public Perl APIs into a tracked matrix. | Disagreement over whether experimental or partially implemented C# APIs count toward the finish line. | Not started | -| 12 | [Export package-manager parity methods](#step-12-export-package-manager-parity-methods) | Export `WithCarton` and `WithProjectDependencies` and confirm their generated TypeScript shapes are acceptable. | `cartonDeployment` option shape or Carton/WithPackage interaction may need polyglot-specific handling. | Not started | -| 13 | [Validate project-dependency scenarios](#step-13-validate-project-dependency-scenarios) | Add TypeScript AppHost examples and tests for `cpanm --installdeps` and Carton-based dependency flows. | Missing `cpanfile` fixtures, `cpanfile.snapshot` handling, or environment prerequisites may make the first scenario too optimistic. | Not started | -| 14 | [Design and export the Perlbrew surface](#step-14-design-and-export-the-perlbrew-surface) | Decide whether TypeScript should expose `WithPerlbrew`, `WithPerlbrewEnvironment`, or a single canonical method, then export it. | Alias duplication and Windows-specific failure semantics may make the raw C# surface too awkward for polyglot callers. | Not started | +| 11 | [Lock the full-capability finish line](#step-11-lock-the-full-capability-finish-line) | Define exactly what counts toward 100% TypeScript capability coverage and turn the remaining public Perl APIs into a tracked matrix. | Remaining gaps now come from unexported or unvalidated public APIs, not from policy ambiguity about experimental surface area. | Complete | +| 12 | [Export package-manager parity methods](#step-12-export-package-manager-parity-methods) | Export `WithCarton` and `WithProjectDependencies` and confirm their generated TypeScript shapes are acceptable. | `cartonDeployment` option shape or Carton/WithPackage interaction may need polyglot-specific handling. | Complete | +| 13 | [Validate project-dependency scenarios](#step-13-validate-project-dependency-scenarios) | Add TypeScript AppHost examples and tests for `cpanm --installdeps` and Carton-based dependency flows. | The scenarios now depend on normal environment prerequisites (`perl`, `cpanm`, `carton`, Aspire restore) rather than missing fixture work. | Complete | +| 14 | [Design and export the Perlbrew surface](#step-14-design-and-export-the-perlbrew-surface) | Export both public Perlbrew helpers and validate their Linux/Windows behavior in TypeScript. | Linux-only runtime behavior and Windows-specific failure semantics still need an explicit validation story. | Not started | | 15 | [Validate Perlbrew across platforms](#step-15-validate-perlbrew-across-platforms) | Add Linux-positive TypeScript validation and explicit Windows behavior checks for the Perlbrew flow. | Perlbrew is Linux-only, so the validation matrix likely needs platform-aware test gating or a Linux-only lane. | Not started | -| 16 | [Export the certificate-trust capability](#step-16-export-the-certificate-trust-capability) | Export `WithPerlCertificateTrust` intentionally and decide how experimental API status should surface in the TypeScript story. | Experimental API policy or ATS handling may require an explicit support decision before export. | Not started | +| 16 | [Export the certificate-trust capability](#step-16-export-the-certificate-trust-capability) | Export `WithPerlCertificateTrust` as part of the public parity target and validate its TypeScript behavior. | End-to-end validation may require more infrastructure than the export itself. | Not started | | 17 | [Validate certificate-trust behavior](#step-17-validate-certificate-trust-behavior) | Add a TypeScript scenario that proves certificate bundle propagation to both runtime and installer paths. | A realistic HTTPS fixture and cross-platform certificate handling may be harder than the export itself. | Not started | | 18 | [Finish module and executable support in C#](#step-18-finish-module-and-executable-support-in-c) | Close the underlying C# behavior gaps for `AddPerlModule` and `AddPerlExecutable` before exporting them. | The methods exist today, but runtime, publish, or sample-level behavior may still be under-specified for full support. | Not started | | 19 | [Export and validate module and executable entry points](#step-19-export-and-validate-module-and-executable-entry-points) | Export `AddPerlModule` and `AddPerlExecutable` only after the C# implementation is promoted to supported behavior. | The generated SDK can be added quickly, but example and runtime proof may lag behind implementation work. | Not started | @@ -469,6 +469,30 @@ Likely blockers 1. The team may need to decide whether experimental and partially implemented APIs count toward “100%” immediately. 2. One alias or convenience method may be intentionally C#-only even if the underlying behavior is supported. +Completion notes + +1. The parity target is now explicit: every public Perl AppHost-facing C# API counts toward the TypeScript finish line, including experimental APIs. +2. Alias-style public APIs stay literal to the C# surface for parity purposes, so `WithPerlbrew` and `WithPerlbrewEnvironment` both remain in-scope rather than being collapsed into one canonical TypeScript method. +3. `WithPerlCertificateTrust` counts toward the finish line immediately even though it remains experimental on the C# side. +4. The matrix below is now the source of truth for what is exported, what is validated, and what remains. + +Finish-line matrix + +| API | Public parity status | TypeScript status | Validation expectation | Remaining work | +| --- | --- | --- | --- | --- | +| `AddPerlApi` | Counts now | Exported | Generated SDK presence, compile-time usage, runtime-backed AppHost scenario | None for current parity slice | +| `AddPerlScript` | Counts now | Exported | Generated SDK presence, compile-time usage, runtime-backed AppHost scenario | None for current parity slice | +| `AddPerlModule` | Counts now | Not yet exported | Generated SDK presence, compile-time usage, runtime-backed AppHost scenario | Export and add dedicated TypeScript validation | +| `AddPerlExecutable` | Counts now | Not yet exported | Generated SDK presence, compile-time usage, runtime-backed AppHost scenario | Export and add dedicated TypeScript validation | +| `WithCpanMinus` | Counts now | Exported | Generated SDK presence, compile-time usage, runtime-backed AppHost scenario | None for current parity slice | +| `WithPackage` | Counts now | Exported | Generated SDK presence, compile-time usage, runtime-backed AppHost scenario | None for current parity slice | +| `WithLocalLib` | Counts now | Exported | Generated SDK presence, compile-time usage, runtime-backed AppHost scenario | None for current parity slice | +| `WithCarton` | Counts now | Exported | Generated SDK presence, compile-time usage, runtime-backed AppHost scenario | None for current parity slice | +| `WithProjectDependencies` | Counts now | Exported | Generated SDK presence, compile-time usage, runtime-backed AppHost scenario, deployment-lockfile path | None for current parity slice | +| `WithPerlbrew` | Counts now | Not yet exported | Generated SDK presence, compile-time usage, Linux-positive runtime behavior, Windows-negative behavior | Export and validate | +| `WithPerlbrewEnvironment` | Counts now | Not yet exported | Generated SDK presence, compile-time usage, Linux-positive runtime behavior, Windows-negative behavior | Export and validate | +| `WithPerlCertificateTrust` | Counts now, experimental | Not yet exported | Generated SDK presence, compile-time usage, runtime env propagation, installer env propagation | Export and validate | + [Back to roadmap chart](#roadmap-chart) ## Step 12: Export package-manager parity methods @@ -499,6 +523,13 @@ Likely blockers 1. `WithProjectDependencies(cartonDeployment: true)` may generate an options shape that needs a TypeScript-specific overload. 2. The Carton and per-package installer interaction may need clearer guidance in generated or human-facing docs. +Completion notes + +1. Added `AspireExport` annotations to `WithCarton` and `WithProjectDependencies` without changing their existing C# interaction rules. +2. The TypeScript scenario now exercises the intended generated call shape for deployment mode as `withProjectDependencies({ cartonDeployment: true })`. +3. The existing `WithPackage()` and `WithCarton()` incompatibility remains unchanged and continues to be enforced by the underlying C# implementation. +4. Focused package-manager regression coverage passed after the export change: `WithCartonTests` and `WithProjectDependenciesTests` ran green. + [Back to roadmap chart](#roadmap-chart) ## Step 13: Validate project-dependency scenarios @@ -529,18 +560,27 @@ Likely blockers 1. The Carton scenario may need new example fixtures such as `cpanfile` and `cpanfile.snapshot`. 2. Command availability such as `carton` may require new required-command gating in the test matrix. +Completion notes + +1. The TypeScript AppHost example now focuses on a single Carton-backed Perl API resource plus the existing driver resource, instead of registering multiple nearly identical API resources in one AppHost. +2. The C# AppHost in the same example continues to cover the `cpanm` package-install flow, while the TypeScript AppHost now exercises a different public surface area with `withCarton().withProjectDependencies({ cartonDeployment: true })`. +3. The TypeScript AppHost now reuses the shared `examples/perl/cpanm-api-integration/scripts/API.pl` entrypoint; the Carton `cpanfile` and `cpanfile.snapshot` live beside that shared script so `withProjectDependencies()` installs from the real working directory rather than a duplicate fixture app. +4. The shared-harness Perl TypeScript AppHost test now waits for both `perl-api` and `perl-driver`, and it requires `perl` plus `carton` for the TypeScript scenario. +5. Focused runtime validation passed for the simplified Carton scenario through the shared TypeScript AppHost harness after correcting a local user-level NuGet source issue in the validation environment. +6. The full Perl test project also passed after these changes with 174 of 174 tests green. + [Back to roadmap chart](#roadmap-chart) ## Step 14: Design and export the Perlbrew surface Goal -1. Make the Perlbrew TypeScript story intentional instead of blindly mirroring two overlapping C# methods. +1. Export the two public Perlbrew methods into TypeScript and validate them with intentional platform-aware coverage. Do in this chunk -1. Decide whether TypeScript should expose both `WithPerlbrew` and `WithPerlbrewEnvironment`, or only one canonical method. -2. Add exports for the chosen Perlbrew surface, reshaping arguments only if that materially improves TypeScript readability. +1. Export both `WithPerlbrew` and `WithPerlbrewEnvironment` because both public C# methods now count toward the parity target. +2. Reshape arguments only if that materially improves TypeScript readability without dropping either public method. 3. Preserve the current Windows failure behavior and Linux-only support constraints. 4. Confirm the generated method names make the Perlbrew flow understandable without leaking internal C# naming history. @@ -556,7 +596,7 @@ Exit criteria Likely blockers -1. Exporting both Perlbrew methods may create avoidable duplication in the TypeScript surface. +1. The two public Perlbrew methods overlap semantically, so the TypeScript story still needs careful naming and documentation even though both methods remain in scope. 2. Windows-specific failure messaging may complicate what “supported in TypeScript” means for this capability. [Back to roadmap chart](#roadmap-chart) @@ -595,18 +635,18 @@ Likely blockers Goal -1. Decide and implement how `WithPerlCertificateTrust` should participate in the TypeScript surface. +1. Export `WithPerlCertificateTrust` and validate it as part of the public TypeScript parity target. Do in this chunk -1. Confirm whether the experimental certificate-trust API belongs in the 100% parity target now. -2. If yes, add the export and inspect the generated TypeScript shape. +1. Add the export for `WithPerlCertificateTrust` and inspect the generated TypeScript shape. +2. Preserve the fact that the API is still experimental on the C# side while keeping it in-scope for parity. 3. Keep the current installer-propagation and idempotency behavior intact. -4. If the experimental status needs special treatment, document that policy rather than silently deferring the method. +4. Add the corresponding runtime and installer validation plan rather than silently deferring the method. Stop and review when -1. There is a clear support decision for the certificate-trust capability. +1. The exported TypeScript story for certificate trust is explicit and consistent with the public C# surface. 2. The generated TypeScript API does not hide that the method is still experimental if that distinction still matters. Exit criteria @@ -616,8 +656,8 @@ Exit criteria Likely blockers -1. Experimental API policy may require a broader repo-level decision. -2. The export itself is easy, but end-to-end validation may require more infrastructure than the method shape suggests. +1. The export itself is easy, but end-to-end validation may require more infrastructure than the method shape suggests. +2. The final TypeScript story still needs a clear way to communicate that the method is experimental without excluding it from parity. [Back to roadmap chart](#roadmap-chart) diff --git a/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.PackageManager.cs b/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.PackageManager.cs index 1c02fbc18..022b63c58 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.PackageManager.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.PackageManager.cs @@ -169,7 +169,11 @@ internal static bool IsCommandAvailable(string resolvedPath, string? pathValue, /// The resource builder. /// A reference to the . /// +<<<<<<< HEAD [AspireExport] +======= + [AspireExport("withCarton", Description = "Configures the Perl resource to use Carton for project dependency installation")] +>>>>>>> e954eeb2 (Committing these to pivot boxes, this is steps 11-13, I do have a question about if these validate-typescript-apphost.ps1 changes are required. Will interrogate later.) public static IResourceBuilder WithCarton( this IResourceBuilder builder) where TResource : PerlAppResource { @@ -258,7 +262,11 @@ public static IResourceBuilder WithPackage( /// When using Carton with set to true, /// a cpanfile.snapshot must also be present. /// +<<<<<<< HEAD [AspireExport] +======= + [AspireExport("withProjectDependencies", Description = "Installs Perl project dependencies before the resource starts")] +>>>>>>> e954eeb2 (Committing these to pivot boxes, this is steps 11-13, I do have a question about if these validate-typescript-apphost.ps1 changes are required. Will interrogate later.) public static IResourceBuilder WithProjectDependencies( this IResourceBuilder builder, bool cartonDeployment = false) where TResource : PerlAppResource diff --git a/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs index 6002ff822..6bfd87665 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs @@ -5,16 +5,18 @@ namespace CommunityToolkit.Aspire.Hosting.Perl.Tests; public class TypeScriptAppHostTests { [Fact] - public async Task TypeScriptAppHostCompilesAndStarts() + public async Task TypeScriptAppHostCartonProjectDependencyScenario_FleetingEndpoint_ReturnsExpectedText() { await TypeScriptAppHostTest.Run( appHostProject: "CpanmApiIntegration.AppHost.TypeScript", packageName: "CommunityToolkit.Aspire.Hosting.Perl", exampleName: "perl/cpanm-api-integration", - waitForResources: ["perl-api"], - waitStatus: "up", - requiredCommands: ["perl", "cpanm"], + waitForResources: ["perl-api", "perl-driver"], + requiredCommands: ["perl", "carton"], useConfiguredPackages: true, + httpProbeResource: "perl-api", + httpProbePath: "/fleeting", + httpProbeExpectedText: "fragile", cancellationToken: TestContext.Current.CancellationToken); } } \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Testing/TypeScriptAppHostTest.cs b/tests/CommunityToolkit.Aspire.Testing/TypeScriptAppHostTest.cs index 6cf1cfb30..62153153e 100644 --- a/tests/CommunityToolkit.Aspire.Testing/TypeScriptAppHostTest.cs +++ b/tests/CommunityToolkit.Aspire.Testing/TypeScriptAppHostTest.cs @@ -15,6 +15,10 @@ public static class TypeScriptAppHostTest /// The Aspire resource status to wait for. /// Optional commands that must exist on PATH before validation runs. /// to validate the AppHost using the package mappings already present in aspire.config.json instead of packing a local polyglot package first. + /// Optional resource display name to probe over HTTP after startup validation completes. + /// Optional relative path to request from the probed HTTP endpoint. + /// Optional exact response text expected from the HTTP probe. + /// The named endpoint to probe when is provided. /// Optional dictionary of secret key-value pairs to set via aspire secret set before starting the app host. /// The cancellation token. public static async Task Run( @@ -25,6 +29,10 @@ public static async Task Run( string waitStatus = "healthy", IEnumerable? requiredCommands = null, bool useConfiguredPackages = false, + string? httpProbeResource = null, + string? httpProbePath = null, + string? httpProbeExpectedText = null, + string httpProbeEndpointName = "http", Dictionary? secrets = null, CancellationToken cancellationToken = default) { @@ -38,6 +46,25 @@ public static async Task Run( throw new ArgumentException("Wait status must be one of 'healthy', 'up', or 'down'.", nameof(waitStatus)); } + bool hasHttpProbeConfiguration = + !string.IsNullOrWhiteSpace(httpProbeResource) + || !string.IsNullOrWhiteSpace(httpProbePath) + || httpProbeExpectedText is not null; + + if (hasHttpProbeConfiguration && + (string.IsNullOrWhiteSpace(httpProbeResource) + || string.IsNullOrWhiteSpace(httpProbePath) + || httpProbeExpectedText is null)) + { + throw new ArgumentException( + "HTTP probing requires a resource name, request path, and expected response text."); + } + + if (hasHttpProbeConfiguration && string.IsNullOrWhiteSpace(httpProbeEndpointName)) + { + throw new ArgumentException("HTTP probe endpoint name cannot be empty.", nameof(httpProbeEndpointName)); + } + List resources = waitForResources .Where(static resource => !string.IsNullOrWhiteSpace(resource)) .ToList(); @@ -82,6 +109,18 @@ public static async Task Run( arguments.Add("-UseConfiguredPackages"); } + if (hasHttpProbeConfiguration) + { + arguments.Add("-HttpProbeResource"); + arguments.Add(httpProbeResource!); + arguments.Add("-HttpProbePath"); + arguments.Add(httpProbePath!); + arguments.Add("-HttpProbeExpectedText"); + arguments.Add(httpProbeExpectedText!); + arguments.Add("-HttpProbeEndpointName"); + arguments.Add(httpProbeEndpointName); + } + if (secrets is { Count: > 0 }) { arguments.Add("-Secrets"); From 85b7bd2cd4f0f0f6edb8a66743ce68b15f91469c Mon Sep 17 00:00:00 2001 From: Matthew Austew Date: Tue, 26 May 2026 22:54:21 -0500 Subject: [PATCH 05/11] These changes are the minimum required to get perl typescript support, I believe. --- eng/testing/validate-typescript-apphost.ps1 | 67 +- .../apphost.ts | 5 +- .../package-lock.json | 5 + .../perl/cpanm-api-integration/scripts/API.pl | 7 + perl-typescript-support-plan.md | 135 --- perl-typescript-support-roadmap.md | 783 ------------------ .../PerlAppResourceBuilderExtensions.cs | 17 +- .../AddPerlExecutableTests.cs | 21 + .../TypeScriptAppHostTests.cs | 16 + .../WithCertificateTrustTests.cs | 20 + .../TypeScriptAppHostTest.cs | 20 +- 11 files changed, 159 insertions(+), 937 deletions(-) delete mode 100644 perl-typescript-support-plan.md delete mode 100644 perl-typescript-support-roadmap.md diff --git a/eng/testing/validate-typescript-apphost.ps1 b/eng/testing/validate-typescript-apphost.ps1 index 94cf5e02a..759589e39 100644 --- a/eng/testing/validate-typescript-apphost.ps1 +++ b/eng/testing/validate-typescript-apphost.ps1 @@ -11,6 +11,8 @@ param( [switch]$UseConfiguredPackages, + [switch]$SkipStart, + [string[]]$WaitForResources = @(), [string[]]$RequiredCommands = @(), @@ -89,13 +91,14 @@ function Invoke-ExternalCommandCaptureOutput { $resolvedFilePath = Resolve-ExternalCommandPath $FilePath $output = & $resolvedFilePath @Arguments 2>&1 + $outputText = (($output | ForEach-Object { $_.ToString() }) -join [Environment]::NewLine) if ($LASTEXITCODE -ne 0) { $joinedArguments = [string]::Join(" ", $Arguments) throw "Command failed with exit code ${LASTEXITCODE}: $FilePath $joinedArguments" } - return (($output | ForEach-Object { $_.ToString() }) -join [Environment]::NewLine) + return $outputText } function Invoke-CleanupStep { @@ -164,14 +167,59 @@ function ConvertFrom-AspireJsonOutput { ) $jsonStartIndex = $Text.IndexOf('{') - $jsonEndIndex = $Text.LastIndexOf('}') + while ($jsonStartIndex -ge 0) { + $depth = 0 + $inString = $false + $isEscaped = $false + + for ($index = $jsonStartIndex; $index -lt $Text.Length; $index++) { + $character = $Text[$index] + + if ($isEscaped) { + $isEscaped = $false + continue + } + + if ($character -eq '\\') { + if ($inString) { + $isEscaped = $true + } + + continue + } + + if ($character -eq '"') { + $inString = -not $inString + continue + } + + if ($inString) { + continue + } + + if ($character -eq '{') { + $depth++ + continue + } + + if ($character -eq '}') { + $depth-- + if ($depth -eq 0) { + $jsonText = $Text.Substring($jsonStartIndex, ($index - $jsonStartIndex) + 1) + try { + return $jsonText | ConvertFrom-Json -AsHashtable -Depth 100 + } + catch { + break + } + } + } + } - if ($jsonStartIndex -lt 0 -or $jsonEndIndex -lt $jsonStartIndex) { - throw "Could not find a JSON payload in Aspire command output." + $jsonStartIndex = $Text.IndexOf('{', $jsonStartIndex + 1) } - $jsonText = $Text.Substring($jsonStartIndex, ($jsonEndIndex - $jsonStartIndex) + 1) - return $jsonText | ConvertFrom-Json -AsHashtable -Depth 100 + throw "Could not find a JSON payload in Aspire command output." } function Get-ResourceEndpointUrl { @@ -242,7 +290,6 @@ if (-not $UseConfiguredPackages) { } $appHostDirectory = Split-Path -Parent $resolvedAppHostPath -$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\\..")).Path $configPath = Join-Path $appHostDirectory "aspire.config.json" $nugetConfigPath = Join-Path $appHostDirectory "nuget.config" $generatedRestoreRoot = Join-Path $appHostDirectory ".aspire\integrations\package-restore" @@ -417,6 +464,10 @@ try { "--log-level", "debug" ) Invoke-ExternalCommand "npx" @("tsc", "--noEmit") + + if ($SkipStart) { + return + } } finally { Pop-Location @@ -459,7 +510,7 @@ try { "describe", "--apphost", $resolvedAppHostPath, "--format", "Json", - "--log-level", "debug" + "--log-level", "warning" ) Write-Output $describeOutput diff --git a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/apphost.ts b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/apphost.ts index 4be032ded..2a7e9650a 100644 --- a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/apphost.ts +++ b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/apphost.ts @@ -1,4 +1,4 @@ -import { createBuilder } from "./.modules/aspire.js"; +import { CertificateTrustScope, createBuilder } from "./.modules/aspire.js"; const builder = await createBuilder(); @@ -6,6 +6,9 @@ const cartonProjectApi = await builder.addPerlApi("perl-api", "../scripts", "API await cartonProjectApi.withCarton(); await cartonProjectApi.withProjectDependencies({ cartonDeployment: true }); await cartonProjectApi.withLocalLib({ path: "local" }); +await cartonProjectApi.withDeveloperCertificateTrust(true); +await cartonProjectApi.withCertificateTrustScope(CertificateTrustScope.Append); +await cartonProjectApi.withPerlCertificateTrust(); await cartonProjectApi.withHttpEndpoint({ name: "http", env: "PORT" }); const perlDriver = await builder.addPerlScript("perl-driver", "../scripts", "driver.pl"); diff --git a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/package-lock.json b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/package-lock.json index 5691f71de..b54a33af7 100644 --- a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/package-lock.json +++ b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/package-lock.json @@ -713,6 +713,7 @@ "integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.59.4", "@typescript-eslint/types": "8.59.4", @@ -904,6 +905,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1140,6 +1142,7 @@ "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -1845,6 +1848,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -1926,6 +1930,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/examples/perl/cpanm-api-integration/scripts/API.pl b/examples/perl/cpanm-api-integration/scripts/API.pl index ce88f2d43..c54f9431f 100644 --- a/examples/perl/cpanm-api-integration/scripts/API.pl +++ b/examples/perl/cpanm-api-integration/scripts/API.pl @@ -12,4 +12,11 @@ $c->render(text => 'fragile'); }; +get '/cert-env' => sub ($c) { + my @required = qw(SSL_CERT_FILE PERL_LWP_SSL_CA_FILE MOJO_CA_FILE); + my $has_all = !grep { !defined $ENV{$_} || $ENV{$_} eq '' } @required; + + $c->render(text => $has_all ? 'present' : 'missing'); +}; + app->start('daemon', map { ('-l', $_) } @listeners); diff --git a/perl-typescript-support-plan.md b/perl-typescript-support-plan.md deleted file mode 100644 index bbda29d14..000000000 --- a/perl-typescript-support-plan.md +++ /dev/null @@ -1,135 +0,0 @@ -# Perl TypeScript AppHost Support Plan - -## Goal - -Enable `CommunityToolkit.Aspire.Hosting.Perl` to work from a TypeScript AppHost, using the current `examples/perl/cpanm-api-integration` sample as the first validated scenario. - -## What The Aspire Docs Say - -- `aspire docs get multi-language-integrations`: - - TypeScript AppHost support comes from ATS annotations on the .NET hosting integration. - - The CLI scans `[AspireExport]` methods and types, then generates the TypeScript SDK into `.modules/`. - - ATS diagnostics such as `ASPIREATS001` are the build-time guardrail for export mistakes. - - Capability IDs must be unique; when needed, use distinct export IDs plus `MethodName` to keep the generated TypeScript names clean. -- `aspire docs get multi-language-architecture`: - - TypeScript AppHosts are guest processes that talk to the .NET host over local JSON-RPC. - - We do not hand-write TypeScript bindings; the SDK is generated from the exported C# surface. -- `aspire docs get typescript-apphost-project-structure`: - - A TypeScript AppHost needs `apphost.ts`, `aspire.config.json`, `package.json`, and `tsconfig.json`. - - Local development can reference a hosting integration by `.csproj` path in `aspire.config.json`. - - `.modules/` is generated and should not be edited directly. - -## Repo Findings - -- The Perl integration currently has no ATS annotations in `src/CommunityToolkit.Aspire.Hosting.Perl/**`. -- The current Perl sample logic lives in `examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost/AppHost.cs` and uses: - - `AddPerlApi` - - `WithCpanMinus` - - `WithPackage` - - `WithLocalLib` - - `AddPerlScript` - - `WithEnvironment` - - `WithReference` - - `WaitFor` -- Existing integrations with TypeScript support follow two patterns: - - Export add-methods with `[AspireExport(...)]`. - - Add ATS-friendly overloads or `[AspireExportIgnore]` when the public C# overload is not suitable for polyglot callers. -- Existing ATS-enabled integrations in this repo currently rely on the diagnostics that already come with the Aspire hosting toolchain; they do not add a separate analyzer package reference in the project file. -- Shared TypeScript validation already exists in `tests/CommunityToolkit.Aspire.Testing/TypeScriptAppHostTest.cs`, so Perl only needs a new test case instead of new harness code. - -## Implementation Plan - -1. Enable ATS in the Perl integration project. - - - Align the Perl project with the current repo ATS pattern rather than introducing a standalone analyzer package. - - Suppress `ASPIREATS001` in the Perl project file, matching the pattern used by other ATS-exporting integrations. - - Rely on the existing Aspire hosting toolchain to surface ATS diagnostics during build. - -2. Export the Perl surface needed by a TypeScript AppHost. - - Add `[AspireExport]` to the entry points in `src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.cs`: - - `AddPerlScript` - - `AddPerlApi` - - Keep `AddPerlModule` and `AddPerlExecutable` out of the TypeScript surface for now because they are not part of the first validated scenario and are not yet fully implemented in C#. - - Export the fluent methods required by the first sample: - - `WithCpanMinus` - - `WithPackage` - - `WithLocalLib` - - Strong candidates for broader parity after the first pass: - - `WithCarton` - - `WithProjectDependencies` - - `WithPerlbrew` / `WithPerlbrewEnvironment` - - Expect to validate whether ATS accepts the current generic Perl fluent methods as-is. - - If the analyzer rejects them or the generated surface is awkward, add concrete `PerlAppResource` polyglot overloads. - - Use distinct export IDs and `MethodName` where needed. - - Use `[AspireExportIgnore]` on overloads that should stay C#-only. - -3. Add the first TypeScript AppHost example. - - Create `examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/`. - - Include: - - `apphost.ts` - - `aspire.config.json` - - `package.json` - - `tsconfig.json` - - `eslint.config.mjs` - - `package-lock.json` if we keep parity with the committed TypeScript examples already in the repo - - In `aspire.config.json`, reference the local integration project: - - `CommunityToolkit.Aspire.Hosting.Perl`: `../../../../src/CommunityToolkit.Aspire.Hosting.Perl/CommunityToolkit.Aspire.Hosting.Perl.csproj` - - Mirror the current C# sample behavior: - - Perl API resource using `cpanm`, `Mojolicious::Lite`, `local::lib`, and an HTTP endpoint - - Perl driver script with environment/reference/wait wiring - - One implementation detail to verify after export generation: - - Confirm the generated endpoint handle syntax for wiring the API URL into `withEnvironment` by inspecting `.modules/aspire.ts` after `aspire restore`. - -4. Add TypeScript AppHost validation. - - Add `tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs`. - - Use the shared harness with: - - `appHostProject: "CpanmApiIntegration.AppHost.TypeScript"` - - `packageName: "CommunityToolkit.Aspire.Hosting.Perl"` - - `exampleName: "perl/cpanm-api-integration"` - - Initial validation settings should be: - - `waitForResources: ["perl-api"]` - - `waitStatus: "up"` - - `requiredCommands: ["perl", "cpanm"]` - - Rationale: - - The sample API has an HTTP endpoint but no explicit health check, so `up` is safer than `healthy`. - - The driver script may be short-lived, so it is not the best primary wait target for the first pass. - - No test project file change should be needed unless we discover explicit compile includes later. - -5. Validate the end-to-end flow. - - Build the Perl integration project. - - Run the Perl test project. - - Run the new `TypeScriptAppHostTests`. - - In the TypeScript example directory: - - `npm ci` - - `aspire restore` - - inspect `.modules/aspire.ts` - - `npx tsc --noEmit` - - Confirm there are no ATS analyzer collisions and that the generated TypeScript names match the intended API shape. - -## Recommended Cut Line - -- Minimum viable TypeScript support for the first implementation pass: - - ATS analyzer wiring - - export the add-methods plus the three fluent methods used by the sample (`WithCpanMinus`, `WithPackage`, `WithLocalLib`) - - create the TypeScript example - - add the TypeScript AppHost test -- Defer until the first slice is green: - - exporting the full Perl surface - - `Perlbrew` support in TypeScript - - certificate-trust/export edge cases - - extra sample permutations beyond `cpanm-api-integration` - -## Risks And Open Questions - -- The main technical risk is ATS compatibility of the current generic Perl fluent methods. -- If `WithPackage` or `WithProjectDependencies` generate an awkward TypeScript signature, we may want dedicated polyglot overloads instead of exporting the raw C# shape. -- If the generated SDK does not make endpoint values directly consumable for `withEnvironment`, the sample may need a small adjustment from the current C# AppHost. -- Because analyzer versions are managed centrally in this repo, the package-version change will likely need to happen in `Directory.Packages.props`, not just the Perl project. - -## First Implementation Order - -1. Add analyzer/version plumbing. -2. Export `AddPerlApi`, `AddPerlScript`, `WithCpanMinus`, `WithPackage`, and `WithLocalLib`. -3. Generate the TypeScript example and inspect `.modules/aspire.ts`. -4. Adjust export shapes only if the analyzer or generated SDK forces it. -5. Add the TypeScript AppHost test and validate it. diff --git a/perl-typescript-support-roadmap.md b/perl-typescript-support-roadmap.md deleted file mode 100644 index 7afbe0f4c..000000000 --- a/perl-typescript-support-roadmap.md +++ /dev/null @@ -1,783 +0,0 @@ -# Perl TypeScript Support Migration Roadmap - -This roadmap turns the existing support plan in [perl-typescript-support-plan.md](perl-typescript-support-plan.md) into reviewable execution chunks for adding TypeScript AppHost support to CommunityToolkit.Aspire.Hosting.Perl. The target started with the current `examples/perl/cpanm-api-integration` scenario first, and the finish line is now the full public Perl AppHost-facing C# surface: every public API should be exported to TypeScript, including experimental APIs, while still preferring TypeScript-friendly shapes where overload translation is needed. Based on the current plan and the existing ATS patterns already used elsewhere in the repo, there is no structural reason this migration cannot be made; the main risks are export-shape compatibility, generated SDK ergonomics, and environment-level validation. - -Please as we do work on this roadmap's steps, keep the document up to date as we make changes. - -## Roadmap Chart - -| Step | Name | Small details | Blockers | Progress | -| --- | --- | --- | --- | --- | -| 1 | [Baseline and scope lock](#step-1-baseline-and-scope-lock) | Confirm the exact C# sample flow, the minimum Perl API surface for MVP, and the explicit non-goals for the first slice. | Hidden sample dependencies or fluent calls that are not yet listed in the support plan. | Complete | -| 2 | [Analyzer and project wiring](#step-2-analyzer-and-project-wiring) | Align the Perl project with the ATS warning and suppression pattern already used by ATS-enabled integrations in the repo. | Repo-specific analyzer behavior may differ from older docs that mention a standalone package. | Complete | -| 3 | [Export surface design](#step-3-export-surface-design) | Decide which APIs export cleanly as-is and which need polyglot overloads, ignores, or renamed methods. | Generic, callback-based, or parameter-ordering-hostile signatures that produce awkward TypeScript. | Complete | -| 4 | [Export add-method entry points](#step-4-export-add-method-entry-points) | Export the top-level Perl resource creation methods needed for the first scenario. | Analyzer rejects a signature or a resource type is not exportable without reshaping. | Complete | -| 5 | [Export fluent methods for the MVP chain](#step-5-export-fluent-methods-for-the-mvp-chain) | Export the fluent methods the sample actually needs, with ATS-specific overloads where needed. | Generic chain methods may need dedicated `PerlAppResource` overloads to stay natural in TypeScript. | Complete | -| 6 | [Generate and inspect the TypeScript SDK](#step-6-generate-and-inspect-the-typescript-sdk) | Restore and inspect `.modules/aspire.ts` before building the TypeScript example around it. | Generated method names, return types, or endpoint handles may not match the intended call style. | Complete | -| 7 | [Scaffold the TypeScript AppHost example](#step-7-scaffold-the-typescript-apphost-example) | Create the TypeScript AppHost project structure and wire it to the local Perl integration project. | Missing config details, package-lock policy decisions, or example-folder conventions. | Complete | -| 8 | [Port the sample behavior idiomatically](#step-8-port-the-sample-behavior-idiomatically) | Recreate the existing sample flow in `apphost.ts` using generated exports, not hand-written wrappers. | Endpoint/reference wiring may expose a rough API shape that needs a return to step 3 or 5. | Complete | -| 9 | [Add automated TypeScript AppHost validation](#step-9-add-automated-typescript-apphost-validation) | Add the Perl-specific test case that uses the shared TypeScript AppHost validation harness. | Wait targets, resource status selection, or command prerequisites may need tuning. | Complete | -| 10 | [Run end-to-end validation and capture follow-up work](#step-10-run-end-to-end-validation-and-capture-follow-up-work) | Execute the narrow and full validation passes, then record any deferred parity work. | Windows Perl environment issues, ATS edge cases, or sample-specific runtime behavior. | Complete | -| 11 | [Lock the full-capability finish line](#step-11-lock-the-full-capability-finish-line) | Define exactly what counts toward 100% TypeScript capability coverage and turn the remaining public Perl APIs into a tracked matrix. | Remaining gaps now come from unexported or unvalidated public APIs, not from policy ambiguity about experimental surface area. | Complete | -| 12 | [Export package-manager parity methods](#step-12-export-package-manager-parity-methods) | Export `WithCarton` and `WithProjectDependencies` and confirm their generated TypeScript shapes are acceptable. | `cartonDeployment` option shape or Carton/WithPackage interaction may need polyglot-specific handling. | Complete | -| 13 | [Validate project-dependency scenarios](#step-13-validate-project-dependency-scenarios) | Add TypeScript AppHost examples and tests for `cpanm --installdeps` and Carton-based dependency flows. | The scenarios now depend on normal environment prerequisites (`perl`, `cpanm`, `carton`, Aspire restore) rather than missing fixture work. | Complete | -| 14 | [Design and export the Perlbrew surface](#step-14-design-and-export-the-perlbrew-surface) | Export both public Perlbrew helpers and validate their Linux/Windows behavior in TypeScript. | Linux-only runtime behavior and Windows-specific failure semantics still need an explicit validation story. | Not started | -| 15 | [Validate Perlbrew across platforms](#step-15-validate-perlbrew-across-platforms) | Add Linux-positive TypeScript validation and explicit Windows behavior checks for the Perlbrew flow. | Perlbrew is Linux-only, so the validation matrix likely needs platform-aware test gating or a Linux-only lane. | Not started | -| 16 | [Export the certificate-trust capability](#step-16-export-the-certificate-trust-capability) | Export `WithPerlCertificateTrust` as part of the public parity target and validate its TypeScript behavior. | End-to-end validation may require more infrastructure than the export itself. | Not started | -| 17 | [Validate certificate-trust behavior](#step-17-validate-certificate-trust-behavior) | Add a TypeScript scenario that proves certificate bundle propagation to both runtime and installer paths. | A realistic HTTPS fixture and cross-platform certificate handling may be harder than the export itself. | Not started | -| 18 | [Finish module and executable support in C#](#step-18-finish-module-and-executable-support-in-c) | Close the underlying C# behavior gaps for `AddPerlModule` and `AddPerlExecutable` before exporting them. | The methods exist today, but runtime, publish, or sample-level behavior may still be under-specified for full support. | Not started | -| 19 | [Export and validate module and executable entry points](#step-19-export-and-validate-module-and-executable-entry-points) | Export `AddPerlModule` and `AddPerlExecutable` only after the C# implementation is promoted to supported behavior. | The generated SDK can be added quickly, but example and runtime proof may lag behind implementation work. | Not started | -| 20 | [Close the parity gap](#step-20-close-the-parity-gap) | Run the final parity audit, add any missing test lanes, and update docs so the supported TypeScript surface is unambiguous. | CI coverage or platform-specific prerequisites may still leave one capability unverified unless the matrix is widened. | Not started | - -## Step 1: Baseline and scope lock - -Goal - -1. Confirm the exact behavior of the current C# sample and turn that into an agreed MVP contract for the TypeScript migration. -2. Make the cut line explicit so review can happen before any ATS annotations are added. - -Do in this chunk - -1. Read the existing C# AppHost sample and enumerate every Perl-specific call in order. -2. Confirm the first-pass exported surface includes `AddPerlApi`, `AddPerlScript`, `WithCpanMinus`, `WithPackage`, and `WithLocalLib`, plus the existing shared Aspire wiring methods already available to polyglot app hosts. -3. Record the first-pass non-goals: full Perl surface parity, Perlbrew support, Carton support, project dependency helpers, and extra example permutations. -4. Confirm what the TypeScript sample must prove: package installation, API startup, environment wiring, reference wiring, and a wait strategy that is stable for automation. - -Stop and review when - -1. There is a written MVP list that a reviewer can sign off on before code changes start. -2. Any method not needed for the first example is explicitly marked as later work instead of silently sliding into scope. - -Exit criteria - -1. The migration target is a single supported scenario with a fixed method inventory. -2. The first implementation cut line is small enough to review comfortably. - -Likely blockers - -1. The current sample may rely on behavior that is only obvious after reading helper methods or nearby resource code. -2. The first scenario may need one extra exported method that is not yet listed in the support plan. - -Completion notes - -1. The first validated scenario is locked to `examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost/AppHost.cs` with no extra Perl-specific calls beyond the ones already identified in the support plan. -2. The Perl-specific call order in the current sample is fixed as: `AddPerlApi`, `WithCpanMinus`, `WithPackage`, `WithLocalLib`, and `AddPerlScript`. -3. The current sample also depends on existing shared Aspire builder methods that should already remain outside the Perl-specific MVP export set: `WithHttpEndpoint`, `WithEnvironment`, `WithReference`, `WaitFor`, and `GetEndpoint`. -4. The MVP Perl-specific export inventory is locked to `AddPerlApi`, `AddPerlScript`, `WithCpanMinus`, `WithPackage`, and `WithLocalLib` for the first TypeScript scenario. -5. The first TypeScript example must prove package installation, Perl API startup, endpoint-to-environment wiring, resource reference wiring, and stable wait behavior for automation. -6. Deferred non-goals remain full Perl surface parity, `AddPerlModule`, `AddPerlExecutable`, Perlbrew helpers, Carton helpers, project dependency helpers, and extra sample permutations beyond `cpanm-api-integration`. -7. No additional Perl-specific blocker was found during the sample read; the remaining uncertainty is export ergonomics rather than hidden sample scope. - -[Back to roadmap chart](#roadmap-chart) - -## Step 2: Analyzer and project wiring - -Goal - -1. Turn on the ATS build-time guardrails in the same way the repo uses them for other polyglot-ready integrations. - -Do in this chunk - -1. Verify how existing ATS-enabled integrations in this repo actually get ATS diagnostics. -2. Apply the same `ASPIREATS001` suppression pattern already used by existing ATS-enabled integrations in this repo. -3. Avoid adding a standalone analyzer package unless the current repo build actually requires it. -4. Build the Perl project to confirm the ATS warnings still flow through the current Aspire hosting toolchain and the project still restores cleanly. - -Stop and review when - -1. The diff is only package-management and project-wiring work. -2. Restore and build behavior are stable before any public API annotations are touched. - -Exit criteria - -1. The Perl integration participates in ATS validation during normal build flows. -2. The Perl project matches the repo's existing ATS project configuration pattern. - -Likely blockers - -1. Older ATS documentation may mention a standalone package that is not used in this repo's current package and feed setup. -2. The Perl project may still need a small repo-specific adjustment if the current build path differs from other ATS-enabled integrations. - -Completion notes - -1. The Perl project now matches the current repo pattern by suppressing `ASPIREATS001` in the project file, consistent with other ATS-enabled integrations. -2. Existing ATS-enabled integrations in this repo do not add a standalone analyzer package reference; the current Aspire hosting toolchain already supplies the relevant ATS diagnostics. -3. Attempting to add standalone analyzer packages failed against the repo's configured feeds, so that path was rejected and removed rather than forcing broader NuGet source changes. -4. The final step 2 configuration kept the repo's existing feed mapping unchanged and aligned the Perl project to local precedent instead. -5. Perl-only validation passed after the final step 2 configuration, so the migration can proceed to export-surface design from a clean baseline. - -[Back to roadmap chart](#roadmap-chart) - -## Step 3: Export surface design - -Goal - -1. Decide the exact exported API shape before implementation so the TypeScript surface feels natural and stable. -2. Make the idiomatic-language rule explicit: keep TypeScript ergonomic even if that means not exposing every C# overload directly. - -Do in this chunk - -1. Review the Perl extension methods that are candidates for export. -2. Classify each method into one of four buckets: export as-is, export with a renamed method, export through a polyglot-specific overload, or ignore for polyglot callers. -3. Apply the same design logic used elsewhere in the repo for polyglot-friendly APIs: avoid callback-based signatures, avoid awkward optional parameter ordering, and avoid exposing types that do not translate well through ATS. -4. Decide where distinct export IDs and `MethodName` values are needed to keep generated TypeScript names clean. -5. Record any methods that should remain C#-only during the first pass. - -Stop and review when - -1. There is a simple export matrix that a reviewer can inspect before the implementation starts. -2. The team agrees that the TypeScript sample should use generated, idiomatic calls rather than force-fitting awkward C# signatures. - -Exit criteria - -1. Every MVP method has an agreed export strategy. -2. The plan is explicit about where `[AspireExportIgnore]` or internal exported overloads may be required. - -Likely blockers - -1. Generic fluent methods may technically export but still produce poor TypeScript ergonomics. -2. Endpoint-related values may need shape adjustments once the generated SDK is inspected. - -Completion notes - -1. The first-pass add-method surface is simple and ATS-friendly as written: `AddPerlApi` and `AddPerlScript` both take only the builder, a resource name, and string path values, so they do not need polyglot-only overloads for the first export pass. -2. No distinct export IDs or `MethodName` overrides are needed for the first-pass add methods because `addPerlApi` and `addPerlScript` already map cleanly to idiomatic TypeScript names. -3. `AddPerlModule` and `AddPerlExecutable` are also string-based, but they remain intentionally deferred because they are outside the validated `cpanm-api-integration` scenario and are not yet fully implemented on the C# side. -4. The MVP fluent methods under consideration remain `WithCpanMinus`, `WithPackage`, and `WithLocalLib`; each currently has scalar arguments only, so the current design assumption is that they can be tried as-is in step 5 before introducing polyglot-specific overloads. -5. `WithCarton` and `WithProjectDependencies` are deferred for first-pass scope reasons rather than ATS-shape reasons. -6. `WithPerlbrew` and `WithPerlbrewEnvironment` are deferred for the first pass because they add a second overlapping concept to the TypeScript surface and are not needed for the first validated scenario. - -Export matrix - -| Method | First-pass decision | Export strategy | Reason | -| --- | --- | --- | --- | -| `AddPerlApi` | Include now | Export as-is | String-only ATS-friendly signature; needed by the sample. | -| `AddPerlScript` | Include now | Export as-is | String-only ATS-friendly signature; needed by the sample. | -| `AddPerlModule` | Defer | None in first pass | Not needed for the MVP sample and not fully implemented in C# yet. | -| `AddPerlExecutable` | Defer | None in first pass | Not needed for the MVP sample and not fully implemented in C# yet. | -| `WithCpanMinus` | Include later in MVP | Try export as-is in step 5 | No callback or complex parameter shape. | -| `WithPackage` | Include later in MVP | Try export as-is in step 5 | Scalar parameters only; ergonomics to confirm after generation. | -| `WithLocalLib` | Include later in MVP | Try export as-is in step 5 | Scalar parameter only; ergonomics to confirm after generation. | -| `WithCarton` | Defer | None in first pass | Broader package-manager surface than the MVP needs. | -| `WithProjectDependencies` | Defer | None in first pass | Broader package-manager surface than the MVP needs. | -| `WithPerlbrew` | Defer | None in first pass | Not needed for MVP and overlaps conceptually with `WithPerlbrewEnvironment`. | -| `WithPerlbrewEnvironment` | Defer | None in first pass | Not needed for MVP and better revisited after the basic TypeScript surface is proven. | - -[Back to roadmap chart](#roadmap-chart) - -## Step 4: Export add-method entry points - -Goal - -1. Make the Perl resource creation entry points available to a TypeScript AppHost. - -Do in this chunk - -1. Add `[AspireExport]` to the agreed Perl add-methods needed for the first implementation pass. -2. Preserve the existing public C# API shape unless a polyglot-specific overload is required. -3. If the analyzer rejects a public signature, add a focused polyglot overload instead of widening the scope of the change. -4. Keep naming aligned with the generated TypeScript method style expected in the sample. - -Stop and review when - -1. Only the top-level add methods are in the diff. -2. Reviewers can confirm naming and surface area before fluent methods are added. - -Exit criteria - -1. The add-method exports compile with the analyzer enabled. -2. The exported entry points cover the first sample's resource creation needs. - -Likely blockers - -1. One of the resource creation methods may depend on a parameter or type that is not ATS-friendly. -2. Method naming may need refinement once the generated TypeScript output is visible. - -Completion notes - -1. `AddPerlScript` now exports directly as `addPerlScript` without a polyglot-specific overload. -2. `AddPerlApi` now exports directly as `addPerlApi` without a polyglot-specific overload. -3. The exported add-method entry points preserved the existing public C# signatures; no parameter reshaping or internal ATS-only overloads were needed at this stage. -4. `AddPerlModule` and `AddPerlExecutable` were intentionally left untouched in this step to preserve the first-pass scope agreed in step 3 and because they are not yet fully implemented in C#. -5. Perl-only validation passed after the add-method exports were added, so the next migration slice can move to the fluent-method exports in step 5. - -[Back to roadmap chart](#roadmap-chart) - -## Step 5: Export fluent methods for the MVP chain - -Goal - -1. Export the chainable Perl methods needed to express the first sample cleanly in TypeScript. - -Do in this chunk - -1. Add exports for `WithCpanMinus`, `WithPackage`, and `WithLocalLib`. -2. Validate whether the current generic signatures are good enough for TypeScript callers. -3. If they are not, add ATS-focused overloads on `IResourceBuilder` or another concrete resource shape so the generated API remains readable. -4. Mark raw C#-only overloads with `[AspireExportIgnore]` where the polyglot call story would otherwise be confusing. -5. Defer broader fluent parity until the first chain is working end to end. - -Stop and review when - -1. The Perl fluent chain used by the sample can be represented without obvious TypeScript friction. -2. Any extra parity work is clearly documented as deferred rather than folded into the MVP. - -Exit criteria - -1. The minimum fluent chain for the sample exists in an exportable form. -2. The code makes an intentional trade-off between C# API fidelity and TypeScript ergonomics where needed. - -Likely blockers - -1. A generic fluent helper may compile but generate an awkward or unstable TypeScript signature. -2. The best polyglot overload may need a separate method name to avoid export collisions. - -Completion notes - -1. `WithCpanMinus`, `WithPackage`, and `WithLocalLib` now export directly from the existing generic methods; no ATS-specific overloads were required for the first pass. -2. The first-pass fluent export set stayed intentionally narrow and did not widen into `WithCarton`, `WithProjectDependencies`, `WithPerlbrew`, or `WithPerlbrewEnvironment`. -3. Perl-only validation passed after the fluent exports were added, so the next check moved to generated SDK shape rather than additional compile-only changes. -4. The remaining question for these methods became purely ergonomic: whether the generated TypeScript signatures read naturally enough for the first sample chain. - -[Back to roadmap chart](#roadmap-chart) - -## Step 6: Generate and inspect the TypeScript SDK - -Goal - -1. Inspect the real generated TypeScript surface before locking in the sample implementation. - -Do in this chunk - -1. Run the generation flow that produces `.modules/aspire.ts` for the new exports. -2. Inspect method names, chaining behavior, parameter order, optional values, and endpoint/reference-related shapes. -3. Confirm the generated API reads like TypeScript and not like a mechanically translated C# API. -4. If the generated surface is awkward, return to step 3 or 5 and fix the exports before building more code on top of them. - -Stop and review when - -1. There is a concrete generated SDK surface to review. -2. The team can decide whether the API shape is good enough before the example is written. - -Exit criteria - -1. The generated SDK supports the desired sample call style. -2. Any remaining shape issues are corrected before the TypeScript example becomes the reference implementation. - -Likely blockers - -1. `withEnvironment` and endpoint handle wiring may need one more export-shape adjustment. -2. Export IDs or method names may collide or read poorly once generation happens. - -Completion notes - -1. A temporary scratch TypeScript AppHost was used to run `aspire restore` against the local Perl project without pulling step 7 into scope early. -2. The generated builder surface kept the exported add methods simple and positional: `addPerlApi(resourceName, appDirectory, scriptName)` and `addPerlScript(resourceName, appDirectory, scriptName)`. -3. The generated fluent surface is acceptable for the MVP sample: - - `withCpanMinus()` stays parameterless. - - `withPackage(packageName, { force, skipTest })` becomes a natural TypeScript call shape. - - `withLocalLib({ path })` becomes an options object with an optional `path` property. -4. The generated `PerlAppResource` surface still includes `withEnvironment`, `withReference`, `waitFor`, and `getEndpoint`, so the planned sample wiring remains viable. -5. The generated `withEnvironment` signature accepts `EndpointReference` and `Awaitable`, which means the planned `API_URL` wiring can use the Perl API endpoint directly. -6. No export-shape correction was needed after SDK inspection, so the migration can proceed to the real TypeScript AppHost scaffold in step 7. - -[Back to roadmap chart](#roadmap-chart) - -## Step 7: Scaffold the TypeScript AppHost example - -Goal - -1. Create the TypeScript AppHost project structure for the Perl sample with the minimum files needed for restore, lint, and type-checking. - -Do in this chunk - -1. Create `examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/`. -2. Add `apphost.ts`, `aspire.config.json`, `package.json`, `tsconfig.json`, and `eslint.config.mjs`. -3. Decide whether `package-lock.json` should be committed for parity with other TypeScript examples in the repo. -4. Point `aspire.config.json` at the local Perl integration project so the generated module comes from the current working tree. -5. Keep the scaffold intentionally small so reviewers can focus on structure before behavior. - -Stop and review when - -1. The example project exists and restores cleanly. -2. The folder layout and config choices match repo conventions well enough to avoid churn later. - -Exit criteria - -1. The TypeScript example can run `npm ci`, `aspire restore`, and `npx tsc --noEmit` with placeholder or near-placeholder app code. -2. The example points at the local Perl integration project rather than a published package. - -Likely blockers - -1. The repo may have an unwritten convention around lock files or ESLint config shape that needs to be copied. -2. TypeScript project structure may need a small adjustment after the first restore. - -Completion notes - -1. The new example folder `examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/` now exists with `apphost.ts`, `aspire.config.json`, `package.json`, `tsconfig.json`, and `eslint.config.mjs`. -2. `aspire.config.json` points at the local Perl hosting project with the same relative `packages` mapping pattern used by the shipped TypeScript examples in this repo. -3. `npm install` was used to generate a committed `package-lock.json`, matching the repo's existing TypeScript example pattern. -4. The scaffold validated successfully with `npm install`, `aspire restore`, and `npm run build` before any Perl-specific behavior was added. -5. The current `apphost.ts` remains intentionally minimal so the next step can focus review on the Perl sample behavior itself rather than structure and config churn. - -[Back to roadmap chart](#roadmap-chart) - -## Step 8: Port the sample behavior idiomatically - -Goal - -1. Recreate the existing Perl sample behavior in `apphost.ts` using the generated API in a way that feels natural to a TypeScript AppHost author. - -Do in this chunk - -1. Mirror the current sample's Perl API resource creation and package-install setup. -2. Add the driver script resource and the needed environment, reference, and wait wiring. -3. Keep the TypeScript code idiomatic: prefer the generated TypeScript names, avoid workarounds that leak raw C# implementation details into the example, and treat awkward generated calls as an export-design issue instead of a sample-authoring issue. -4. Verify how endpoint values should be passed into `withEnvironment` after generation. -5. Keep behavioral parity with the current C# sample while allowing TypeScript syntax and naming to look native. - -Stop and review when - -1. The example expresses the same scenario as the C# sample without obvious API awkwardness. -2. Any place where the example starts to look like a workaround is captured as an export-shape bug to fix. - -Exit criteria - -1. The TypeScript example is readable as a first-class AppHost sample. -2. The example proves the exported surface is good enough for real use, not just technically callable. - -Likely blockers - -1. Endpoint/reference values may not plug into the fluent chain exactly as expected on the first attempt. -2. A hidden assumption in the current C# sample may require one more exported helper. - -Completion notes - -1. The TypeScript AppHost now mirrors the current C# sample behavior with a Perl API resource and a Perl driver resource. -2. The Perl API uses the same first-pass behavior as the C# sample: `addPerlApi`, `withCpanMinus`, `withPackage("Mojolicious::Lite", { force: true, skipTest: true })`, `withLocalLib({ path: "local" })`, and `withHttpEndpoint({ name: "http", env: "PORT" })`. -3. The Perl driver uses `addPerlScript`, `withEnvironment("API_URL", perlApi.getEndpoint("http"))`, `withReference(perlApi)`, and `waitFor(perlApi)`. -4. The only correction needed during validation was the local project path in `aspire.config.json`; once fixed, `aspire restore` and `npm run build` both passed. -5. The generated TypeScript surface remained idiomatic enough that no additional ATS-specific overloads were required for this sample port. - -[Back to roadmap chart](#roadmap-chart) - -## Step 9: Add automated TypeScript AppHost validation - -Goal - -1. Add the Perl-specific automated validation that exercises the new TypeScript AppHost example through the shared test harness. - -Do in this chunk - -1. Add [tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs](tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs). -2. Configure the test to use `CpanmApiIntegration.AppHost.TypeScript`, `CommunityToolkit.Aspire.Hosting.Perl`, and `perl/cpanm-api-integration`. -3. Start with the expected wait target and status from the support plan, then adjust only if real execution proves they are wrong. -4. Include any required command checks such as `perl` and `cpanm`. -5. Keep this step focused on wiring the shared harness, not inventing a new validation path. - -Stop and review when - -1. The test diff is isolated and easy to evaluate. -2. The validation contract for the example is explicit enough for later maintenance. - -Exit criteria - -1. The new test compiles and exercises the shared TypeScript AppHost validation flow. -2. The expected wait resource and status are based on real sample behavior. - -Likely blockers - -1. The API resource may need a different wait target or status than the initial guess. -2. Short-lived helper resources may make the first validation setup too optimistic. - -Completion notes - -1. Added [tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs](tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs) and wired it to `CpanmApiIntegration.AppHost.TypeScript`, `CommunityToolkit.Aspire.Hosting.Perl`, and `perl/cpanm-api-integration`. -2. The validated wait contract is `waitForResources: ["perl-api"]` with `waitStatus: "up"`, plus required command checks for `perl` and `cpanm`. -3. The shared validation harness now supports a `useConfiguredPackages` mode so examples can be validated against the local package mapping already present in `aspire.config.json` when that is the intended development shape. -4. Validation exposed that the Perl TypeScript AppHost was missing the same `profiles` startup metadata used by the other working TypeScript AppHost examples; adding the `https` profile with dashboard and resource-service endpoint settings fixed the detached `aspire start` failure. -5. After those fixes, the focused Perl TypeScript AppHost test passed end to end through restore, TypeScript compilation, `aspire start`, resource wait, and `aspire describe`. - -[Back to roadmap chart](#roadmap-chart) - -## Step 10: Run end-to-end validation and capture follow-up work - -Goal - -1. Prove the full path works and document what remains after the MVP is green. - -Do in this chunk - -1. Run the narrow build and test checks for the touched code. -2. Run the Perl test project and the new TypeScript AppHost test. -3. In the example folder, run `npm ci`, `aspire restore`, inspect `.modules/aspire.ts`, and run `npx tsc --noEmit`. -4. Record the actual results in this roadmap, including any export-shape adjustments or environment-specific issues discovered during validation. -5. Capture the deferred follow-up list: full fluent parity, Perlbrew support, Carton, project dependency helpers, and any Windows-specific environment caveats. - -Stop and review when - -1. There is a complete validation record for the MVP slice. -2. Follow-up work is separated cleanly from the first successful migration. - -Exit criteria - -1. The first TypeScript AppHost Perl scenario is green end to end, or the remaining blockers are concrete and narrowly scoped. -2. The roadmap reflects what was actually learned, not just the original guess. - -Likely blockers - -1. Environment-specific behavior on Windows may require a documented caveat even if the export work is correct. -2. One remaining ATS issue may only become visible after the full example and test flow run together. - -Completion notes - -1. End-to-end example validation passed in the TypeScript AppHost folder with `npm ci`, `aspire restore --apphost apphost.ts --non-interactive`, and `npx tsc --noEmit`. -2. The generated SDK in `.modules/aspire.ts` now confirms the intended Perl-specific surface exactly: `addPerlApi`, `addPerlScript`, `withCpanMinus`, `withPackage`, and `withLocalLib` are present; `addPerlModule` and `addPerlExecutable` are intentionally absent. -3. The missing `addPerlModule` and `addPerlExecutable` methods are not a regression in ATS export wiring; they remain deferred because those entry points are not fully implemented on the C# side and are outside the first validated scenario. -4. The focused TypeScript AppHost validation test passed, and the full Perl test project also passed with 148 of 148 tests green. -5. The remaining deferred follow-up list is broader parity work rather than MVP breakage: Carton helpers, project dependency helpers, Perlbrew-related surface, and any later revisit of module/executable support after the underlying C# implementation is finished. - -[Back to roadmap chart](#roadmap-chart) - -## Step 11: Lock the full-capability finish line - -Goal - -1. Define what 100% TypeScript capability coverage means for the Perl integration so the remaining work can be measured cleanly. -2. Turn the remaining public Perl AppHost APIs into a tracked matrix with an explicit status for each capability. - -Do in this chunk - -1. Inventory every remaining public Perl AppHost-facing API that is not yet available from the generated TypeScript SDK. -2. Split the inventory into three buckets: ready to export now, blocked on a C# implementation gap, and experimental or policy-dependent. -3. Decide whether alias-style APIs such as `WithPerlbrew` should be exported directly, ignored, or collapsed into a single canonical TypeScript method. -4. Decide whether experimental APIs such as `WithPerlCertificateTrust` count toward the 100% parity finish line now or only after an explicit support decision. -5. Record the validation expectation for each capability: generated SDK presence, compile-time usage, runtime scenario, and platform-specific negative behavior where relevant. - -Stop and review when - -1. There is a single finish-line matrix that says exactly what work remains. -2. Reviewers can see which gaps are export-only versus blocked on deeper C# support. - -Exit criteria - -1. The roadmap has a precise remaining capability list instead of a generic parity aspiration. -2. Every later step can point back to a specific capability bucket in this matrix. - -Likely blockers - -1. The team may need to decide whether experimental and partially implemented APIs count toward “100%” immediately. -2. One alias or convenience method may be intentionally C#-only even if the underlying behavior is supported. - -Completion notes - -1. The parity target is now explicit: every public Perl AppHost-facing C# API counts toward the TypeScript finish line, including experimental APIs. -2. Alias-style public APIs stay literal to the C# surface for parity purposes, so `WithPerlbrew` and `WithPerlbrewEnvironment` both remain in-scope rather than being collapsed into one canonical TypeScript method. -3. `WithPerlCertificateTrust` counts toward the finish line immediately even though it remains experimental on the C# side. -4. The matrix below is now the source of truth for what is exported, what is validated, and what remains. - -Finish-line matrix - -| API | Public parity status | TypeScript status | Validation expectation | Remaining work | -| --- | --- | --- | --- | --- | -| `AddPerlApi` | Counts now | Exported | Generated SDK presence, compile-time usage, runtime-backed AppHost scenario | None for current parity slice | -| `AddPerlScript` | Counts now | Exported | Generated SDK presence, compile-time usage, runtime-backed AppHost scenario | None for current parity slice | -| `AddPerlModule` | Counts now | Not yet exported | Generated SDK presence, compile-time usage, runtime-backed AppHost scenario | Export and add dedicated TypeScript validation | -| `AddPerlExecutable` | Counts now | Not yet exported | Generated SDK presence, compile-time usage, runtime-backed AppHost scenario | Export and add dedicated TypeScript validation | -| `WithCpanMinus` | Counts now | Exported | Generated SDK presence, compile-time usage, runtime-backed AppHost scenario | None for current parity slice | -| `WithPackage` | Counts now | Exported | Generated SDK presence, compile-time usage, runtime-backed AppHost scenario | None for current parity slice | -| `WithLocalLib` | Counts now | Exported | Generated SDK presence, compile-time usage, runtime-backed AppHost scenario | None for current parity slice | -| `WithCarton` | Counts now | Exported | Generated SDK presence, compile-time usage, runtime-backed AppHost scenario | None for current parity slice | -| `WithProjectDependencies` | Counts now | Exported | Generated SDK presence, compile-time usage, runtime-backed AppHost scenario, deployment-lockfile path | None for current parity slice | -| `WithPerlbrew` | Counts now | Not yet exported | Generated SDK presence, compile-time usage, Linux-positive runtime behavior, Windows-negative behavior | Export and validate | -| `WithPerlbrewEnvironment` | Counts now | Not yet exported | Generated SDK presence, compile-time usage, Linux-positive runtime behavior, Windows-negative behavior | Export and validate | -| `WithPerlCertificateTrust` | Counts now, experimental | Not yet exported | Generated SDK presence, compile-time usage, runtime env propagation, installer env propagation | Export and validate | - -[Back to roadmap chart](#roadmap-chart) - -## Step 12: Export package-manager parity methods - -Goal - -1. Expose the remaining package-manager capabilities needed for TypeScript parity with the supported Perl package flows. - -Do in this chunk - -1. Add TypeScript exports for `WithCarton` and `WithProjectDependencies`. -2. Inspect the generated SDK shape for `cartonDeployment` and confirm it reads naturally for TypeScript callers. -3. Keep the C# interaction rules intact, especially the existing `WithPackage()` and `WithCarton()` incompatibility. -4. Add ATS-specific overloads or ignore wrappers only if the generated TypeScript API is awkward or misleading. - -Stop and review when - -1. The generated TypeScript package-manager surface is readable without explaining C# implementation details. -2. Negative interaction rules remain explicit rather than hidden in runtime exceptions alone. - -Exit criteria - -1. `WithCarton` and `WithProjectDependencies` appear in `.modules/aspire.ts` with acceptable TypeScript call shapes. -2. The export diff is limited to package-manager parity and does not widen into unrelated capability work. - -Likely blockers - -1. `WithProjectDependencies(cartonDeployment: true)` may generate an options shape that needs a TypeScript-specific overload. -2. The Carton and per-package installer interaction may need clearer guidance in generated or human-facing docs. - -Completion notes - -1. Added `AspireExport` annotations to `WithCarton` and `WithProjectDependencies` without changing their existing C# interaction rules. -2. The TypeScript scenario now exercises the intended generated call shape for deployment mode as `withProjectDependencies({ cartonDeployment: true })`. -3. The existing `WithPackage()` and `WithCarton()` incompatibility remains unchanged and continues to be enforced by the underlying C# implementation. -4. Focused package-manager regression coverage passed after the export change: `WithCartonTests` and `WithProjectDependenciesTests` ran green. - -[Back to roadmap chart](#roadmap-chart) - -## Step 13: Validate project-dependency scenarios - -Goal - -1. Prove the exported package-manager parity surface works in real TypeScript AppHost scenarios, not just in generated type signatures. - -Do in this chunk - -1. Add a TypeScript AppHost scenario for `WithProjectDependencies()` in the cpanm flow. -2. Add a Carton-focused TypeScript AppHost scenario that exercises `.WithCarton().WithProjectDependencies()` with the right `cpanfile` expectations. -3. Add shared-harness tests for both scenarios and keep the wait strategy stable for automation. -4. Cover the `cpanfile.snapshot` requirement and any command prerequisites explicitly in the tests. - -Stop and review when - -1. There is at least one runtime-backed TypeScript AppHost test for each supported project-dependency path. -2. Reviewers can see the difference between type-only coverage and behavior coverage. - -Exit criteria - -1. The TypeScript AppHost test suite proves both cpanm-based and Carton-based project dependency flows. -2. Any missing runtime prerequisite is captured as a concrete blocker rather than a vague parity gap. - -Likely blockers - -1. The Carton scenario may need new example fixtures such as `cpanfile` and `cpanfile.snapshot`. -2. Command availability such as `carton` may require new required-command gating in the test matrix. - -Completion notes - -1. The TypeScript AppHost example now focuses on a single Carton-backed Perl API resource plus the existing driver resource, instead of registering multiple nearly identical API resources in one AppHost. -2. The C# AppHost in the same example continues to cover the `cpanm` package-install flow, while the TypeScript AppHost now exercises a different public surface area with `withCarton().withProjectDependencies({ cartonDeployment: true })`. -3. The TypeScript AppHost now reuses the shared `examples/perl/cpanm-api-integration/scripts/API.pl` entrypoint; the Carton `cpanfile` and `cpanfile.snapshot` live beside that shared script so `withProjectDependencies()` installs from the real working directory rather than a duplicate fixture app. -4. The shared-harness Perl TypeScript AppHost test now waits for both `perl-api` and `perl-driver`, and it requires `perl` plus `carton` for the TypeScript scenario. -5. Focused runtime validation passed for the simplified Carton scenario through the shared TypeScript AppHost harness after correcting a local user-level NuGet source issue in the validation environment. -6. The full Perl test project also passed after these changes with 174 of 174 tests green. - -[Back to roadmap chart](#roadmap-chart) - -## Step 14: Design and export the Perlbrew surface - -Goal - -1. Export the two public Perlbrew methods into TypeScript and validate them with intentional platform-aware coverage. - -Do in this chunk - -1. Export both `WithPerlbrew` and `WithPerlbrewEnvironment` because both public C# methods now count toward the parity target. -2. Reshape arguments only if that materially improves TypeScript readability without dropping either public method. -3. Preserve the current Windows failure behavior and Linux-only support constraints. -4. Confirm the generated method names make the Perlbrew flow understandable without leaking internal C# naming history. - -Stop and review when - -1. The Perlbrew TypeScript API does not present duplicate or confusing entry points. -2. The support decision is explicit enough that later docs and tests can follow it consistently. - -Exit criteria - -1. The Perlbrew capability appears in the generated SDK with a deliberate TypeScript shape. -2. The roadmap records whether alias-style C# methods remain C#-only or are intentionally exported. - -Likely blockers - -1. The two public Perlbrew methods overlap semantically, so the TypeScript story still needs careful naming and documentation even though both methods remain in scope. -2. Windows-specific failure messaging may complicate what “supported in TypeScript” means for this capability. - -[Back to roadmap chart](#roadmap-chart) - -## Step 15: Validate Perlbrew across platforms - -Goal - -1. Prove the Perlbrew TypeScript story works where supported and fails predictably where unsupported. - -Do in this chunk - -1. Add a Linux-positive TypeScript AppHost scenario that exercises the Perlbrew flow end to end. -2. Add explicit coverage for the current Windows path so the expected failure behavior is documented and tested. -3. Extend the shared validation harness or test conventions if platform gating is required. -4. Record any Linux-only prerequisites or CI-lane needs rather than assuming the existing Windows flow is enough. - -Stop and review when - -1. Platform behavior is explicit and repeatable. -2. Reviewers can see what is genuinely supported on Linux and what is intentionally unsupported on Windows. - -Exit criteria - -1. The Perlbrew TypeScript capability has both positive and negative validation where appropriate. -2. Platform-specific requirements are captured in tests or docs rather than tribal knowledge. - -Likely blockers - -1. Perlbrew is Linux-only, so an additional test lane or devcontainer validation path may be required. -2. The existing shared harness may need a small extension to express OS-specific expectations cleanly. - -[Back to roadmap chart](#roadmap-chart) - -## Step 16: Export the certificate-trust capability - -Goal - -1. Export `WithPerlCertificateTrust` and validate it as part of the public TypeScript parity target. - -Do in this chunk - -1. Add the export for `WithPerlCertificateTrust` and inspect the generated TypeScript shape. -2. Preserve the fact that the API is still experimental on the C# side while keeping it in-scope for parity. -3. Keep the current installer-propagation and idempotency behavior intact. -4. Add the corresponding runtime and installer validation plan rather than silently deferring the method. - -Stop and review when - -1. The exported TypeScript story for certificate trust is explicit and consistent with the public C# surface. -2. The generated TypeScript API does not hide that the method is still experimental if that distinction still matters. - -Exit criteria - -1. `WithPerlCertificateTrust` is either intentionally exported with a validation plan or intentionally excluded with an explicit rationale. -2. The roadmap no longer treats certificate trust as an unnamed future item. - -Likely blockers - -1. The export itself is easy, but end-to-end validation may require more infrastructure than the method shape suggests. -2. The final TypeScript story still needs a clear way to communicate that the method is experimental without excluding it from parity. - -[Back to roadmap chart](#roadmap-chart) - -## Step 17: Validate certificate-trust behavior - -Goal - -1. Prove that TypeScript callers can rely on certificate bundle propagation for both runtime and installer flows. - -Do in this chunk - -1. Add a TypeScript scenario that exercises an HTTPS or CA-bundle-dependent flow. -2. Validate that runtime resources receive the expected certificate-related environment variables. -3. Validate that installer child resources also receive the propagated certificate trust settings. -4. Keep the scenario narrow enough that failures identify trust propagation problems rather than unrelated network complexity. - -Stop and review when - -1. The certificate-trust capability is backed by a concrete behavior test. -2. The scenario is simple enough that future regressions are diagnosable. - -Exit criteria - -1. The TypeScript AppHost suite contains at least one end-to-end certificate-trust proof. -2. Any platform-specific caveat is documented directly in the roadmap. - -Likely blockers - -1. Creating a realistic HTTPS or custom-CA test fixture may take more setup than the export work itself. -2. Cross-platform certificate bundle handling may require separate validation assumptions for Windows and Linux. - -[Back to roadmap chart](#roadmap-chart) - -## Step 18: Finish module and executable support in C# - -Goal - -1. Promote `AddPerlModule` and `AddPerlExecutable` from “builder exists” to “behavior is fully supported” before exposing them in TypeScript. - -Do in this chunk - -1. Identify the exact implementation gaps that currently make module and executable support feel incomplete. -2. Add or strengthen runtime, publish-mode, and integration coverage for those entry points on the C# side. -3. Decide whether both methods truly belong in the long-term supported surface or whether one should remain internal or explicitly unsupported. -4. Avoid exporting them to TypeScript until the C# story is no longer caveated. - -Stop and review when - -1. The underlying C# support decision is settled. -2. Reviewers can see that TypeScript export is no longer leading the C# implementation. - -Exit criteria - -1. `AddPerlModule` and `AddPerlExecutable` are either promoted to fully supported behavior or explicitly carved out of the parity target with written justification. -2. The roadmap no longer relies on vague “not fully implemented” language. - -Likely blockers - -1. The current behavior may be adequate for unit tests but not for realistic runtime or publish scenarios. -2. The supporting examples and validation fixtures may not exist yet. - -[Back to roadmap chart](#roadmap-chart) - -## Step 19: Export and validate module and executable entry points - -Goal - -1. Expose module and executable entry points to TypeScript only after step 18 confirms they are genuinely supported. - -Do in this chunk - -1. Add exports for `AddPerlModule` and `AddPerlExecutable` once their C# behavior is approved. -2. Inspect the generated TypeScript SDK to ensure the entry points read naturally and do not need polyglot-only reshaping. -3. Add at least one TypeScript AppHost scenario for the module flow and one for the executable flow. -4. Add automated validation that proves those entry points compile and run rather than merely appearing in `.modules/aspire.ts`. - -Stop and review when - -1. The generated SDK and the example behavior agree on what “supported” means. -2. Reviewers can validate both new entry points independently. - -Exit criteria - -1. `addPerlModule` and `addPerlExecutable` are present in the generated SDK only after their runtime behavior is proven. -2. The final TypeScript capability matrix includes behavior coverage for these entry points, not just export coverage. - -Likely blockers - -1. Example fixtures for module and executable flows may need to be created from scratch. -2. Publish-mode or deployment assumptions may still diverge from simple local run-mode validation. - -[Back to roadmap chart](#roadmap-chart) - -## Step 20: Close the parity gap - -Goal - -1. Finish the TypeScript parity work with a documented, test-backed answer to “what is fully covered now?” - -Do in this chunk - -1. Generate a final capability matrix that compares the intended supported C# Perl AppHost APIs to the generated TypeScript SDK surface. -2. Run the full Perl test project and the expanded TypeScript AppHost validation set. -3. Add any missing CI or platform-specific validation lane needed to keep the new capabilities from regressing. -4. Update the roadmap, support plan, README, and any sample docs so the supported TypeScript surface is unambiguous. -5. Record the residual exclusions, if any, as explicit product decisions rather than undocumented omissions. - -Stop and review when - -1. The supported TypeScript surface can be described in one precise list. -2. There is no remaining disagreement over whether the roadmap is complete. - -Exit criteria - -1. Every supported Perl TypeScript capability has generated-SDK coverage and the right level of behavior validation. -2. The final documentation matches the actual shipped TypeScript surface and the CI matrix can keep it that way. - -Likely blockers - -1. Linux-only or HTTPS-specific capabilities may require extra CI wiring before the parity claim is fully defensible. -2. One capability may still need a product decision even after the technical work is understood. - -[Back to roadmap chart](#roadmap-chart) diff --git a/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.cs index a13f91806..50acaf2b7 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.cs @@ -110,7 +110,7 @@ public static IResourceBuilder AddPerlApi( /// builder.Build().Run(); /// /// - [AspireExport] + [AspireExport("addPerlModule", Description = "Adds a Perl module resource")] public static IResourceBuilder AddPerlModule( this IDistributedApplicationBuilder builder, [ResourceName] string resourceName, string appDirectory, string moduleName) => AddPerlAppCore(builder, resourceName, appDirectory, EntrypointType.Module, moduleName, DefaultPerlEnvironment); @@ -131,7 +131,7 @@ public static IResourceBuilder AddPerlModule( /// builder.Build().Run(); /// /// - [AspireExport] + [AspireExport("addPerlExecutable", Description = "Adds a Perl executable resource")] public static IResourceBuilder AddPerlExecutable( this IDistributedApplicationBuilder builder, [ResourceName] string resourceName, string appDirectory, string executablePath) => AddPerlAppCore(builder, resourceName, appDirectory, EntrypointType.Executable, executablePath, DefaultPerlEnvironment); @@ -215,8 +215,11 @@ private static IResourceBuilder AddPerlAppCore( resourceBuilder.WithOtlpExporter(); - resourceBuilder.WithRequiredCommand("perl", "https://www.perl.org/get.html"); - resourceBuilder.WithRequiredCommand("cpan", "https://metacpan.org/pod/CPAN"); + if (entrypointType != EntrypointType.Executable) + { + resourceBuilder.WithRequiredCommand("perl", "https://www.perl.org/get.html"); + resourceBuilder.WithRequiredCommand("cpan", "https://metacpan.org/pod/CPAN"); + } // Configure OpenTelemetry exporters using environment variables // https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#exporter-selection @@ -326,7 +329,7 @@ private static void AddWaitIfMissing(IResource targetResource, IResource depende /// the PERLBREW_ROOT environment variable, or defaults to ~/perl5/perlbrew. /// /// A reference to the . - [AspireExport] + [AspireExport("withPerlbrew", Description = "Configures the Perl resource to use a perlbrew-managed Perl version")] public static IResourceBuilder WithPerlbrew( this IResourceBuilder builder, string version, string? perlbrewRoot = null) where T : PerlAppResource => builder.WithPerlbrewEnvironment(version, perlbrewRoot); @@ -357,7 +360,7 @@ public static IResourceBuilder WithPerlbrew( /// and enabling per-project module isolation. /// /// - [AspireExport] + [AspireExport("withPerlbrewEnvironment", Description = "Configures the Perl resource to use a perlbrew-managed Perl version and environment")] public static IResourceBuilder WithPerlbrewEnvironment( this IResourceBuilder builder, string version, string? perlbrewRoot = null) where T : PerlAppResource { @@ -502,7 +505,7 @@ public static IResourceBuilder WithLocalLib( /// The resource builder. /// A reference to the . [Experimental("CTASPIREPERL001")] - [AspireExport] + [AspireExport("withPerlCertificateTrust", Description = "Configures Perl certificate trust using Aspire-provided certificate bundle settings")] public static IResourceBuilder WithPerlCertificateTrust( this IResourceBuilder builder) where TResource : PerlAppResource { diff --git a/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/AddPerlExecutableTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/AddPerlExecutableTests.cs index b075c7c73..60d1546f2 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/AddPerlExecutableTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/AddPerlExecutableTests.cs @@ -1,4 +1,5 @@ using Aspire.Hosting; +using Aspire.Hosting.ApplicationModel; using CommunityToolkit.Aspire.Hosting.Perl.Annotations; namespace CommunityToolkit.Aspire.Hosting.Perl.Tests; @@ -36,4 +37,24 @@ public void AddPerlExecutableShouldThrowWhenBuilderIsNull() Assert.Throws(() => builder.AddPerlExecutable("perl-bin", "bin", "my-compiled-perl")); } + + [Fact] + public void AddPerlExecutable_DoesNotRegisterInterpreterRequiredCommands() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddPerlExecutable("perl-bin", "bin", "my-compiled-perl"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + +#pragma warning disable ASPIRECOMMAND001 + var requiredCommands = resource.Annotations.OfType().ToList(); +#pragma warning restore ASPIRECOMMAND001 + + Assert.DoesNotContain(requiredCommands, annotation => annotation.Command == "perl"); + Assert.DoesNotContain(requiredCommands, annotation => annotation.Command == "cpan"); + } } diff --git a/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs index 6bfd87665..6deecae2e 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs @@ -19,4 +19,20 @@ await TypeScriptAppHostTest.Run( httpProbeExpectedText: "fragile", cancellationToken: TestContext.Current.CancellationToken); } + + [Fact] + public async Task TypeScriptAppHostCartonProjectDependencyScenario_CertificateTrustEnv_ReturnsPresent() + { + await TypeScriptAppHostTest.Run( + appHostProject: "CpanmApiIntegration.AppHost.TypeScript", + packageName: "CommunityToolkit.Aspire.Hosting.Perl", + exampleName: "perl/cpanm-api-integration", + waitForResources: ["perl-api", "perl-driver"], + requiredCommands: ["perl", "carton"], + useConfiguredPackages: true, + httpProbeResource: "perl-api", + httpProbePath: "/cert-env", + httpProbeExpectedText: "present", + cancellationToken: TestContext.Current.CancellationToken); + } } \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/WithCertificateTrustTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/WithCertificateTrustTests.cs index 7602dcde8..dc8c57d26 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/WithCertificateTrustTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/WithCertificateTrustTests.cs @@ -108,4 +108,24 @@ public void WithPerlCertificateTrust_BeforeWithPackage_StillPropagates() // WithPerlCertificateTrust() was called before WithPackage() Assert.Single(installerResource.Annotations.OfType()); } + + [Fact] + public void WithPerlCertificateTrust_WithProjectDependencies_PropagatesToProjectInstaller() + { + var builder = DistributedApplication.CreateBuilder(); + +#pragma warning disable CTASPIREPERL001 + builder.AddPerlScript("perl-app", "scripts", "app.pl") + .WithProjectDependencies() + .WithPerlCertificateTrust(); +#pragma warning restore CTASPIREPERL001 + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var installerResource = Assert.Single(appModel.Resources.OfType()); + + Assert.Single(installerResource.Annotations.OfType()); + Assert.Single(installerResource.Annotations.OfType()); + } } diff --git a/tests/CommunityToolkit.Aspire.Testing/TypeScriptAppHostTest.cs b/tests/CommunityToolkit.Aspire.Testing/TypeScriptAppHostTest.cs index 62153153e..d0f9a4256 100644 --- a/tests/CommunityToolkit.Aspire.Testing/TypeScriptAppHostTest.cs +++ b/tests/CommunityToolkit.Aspire.Testing/TypeScriptAppHostTest.cs @@ -1,7 +1,7 @@ namespace CommunityToolkit.Aspire.Testing; /// -/// Runs the shared TypeScript AppHost validation flow for example app hosts. +/// Runs the shared TypeScript AppHost validation flow for example and test app hosts. /// public static class TypeScriptAppHostTest { @@ -20,6 +20,8 @@ public static class TypeScriptAppHostTest /// Optional exact response text expected from the HTTP probe. /// The named endpoint to probe when is provided. /// Optional dictionary of secret key-value pairs to set via aspire secret set before starting the app host. + /// to stop after restore and type-check validation without starting the AppHost. + /// Optional repository-relative path to an apphost.ts file. When omitted, the path is resolved from and . /// The cancellation token. public static async Task Run( string appHostProject, @@ -34,6 +36,8 @@ public static async Task Run( string? httpProbeExpectedText = null, string httpProbeEndpointName = "http", Dictionary? secrets = null, + bool skipStart = false, + string? appHostPathRelativeToRepo = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(appHostProject); @@ -75,7 +79,10 @@ public static async Task Run( string repoRoot = Path.GetFullPath(Path.Combine("..", "..", "..", "..", "..")); string scriptPath = Path.Combine(repoRoot, "eng", "testing", "validate-typescript-apphost.ps1"); - string appHostPath = Path.Combine(repoRoot, "examples", exampleName, appHostProject, "apphost.mts"); + string appHostPath = string.IsNullOrWhiteSpace(appHostPathRelativeToRepo) + ? Path.Combine(repoRoot, "examples", exampleName, appHostProject, "apphost.ts") + : Path.Combine(repoRoot, appHostPathRelativeToRepo); + string packageProjectPath = Path.Combine(repoRoot, "src", packageName, $"{packageName}.csproj"); string shell = OperatingSystem.IsWindows() ? "pwsh.exe" : "pwsh"; List arguments = @@ -83,7 +90,9 @@ public static async Task Run( "-NoLogo", "-NoProfile", "-File", scriptPath, - "-AppHostPath", appHostPath + "-AppHostPath", appHostPath, + "-PackageProjectPath", packageProjectPath, + "-PackageName", packageName ]; if (resources.Count > 0) @@ -109,6 +118,11 @@ public static async Task Run( arguments.Add("-UseConfiguredPackages"); } + if (skipStart) + { + arguments.Add("-SkipStart"); + } + if (hasHttpProbeConfiguration) { arguments.Add("-HttpProbeResource"); From 662fb115f7817797382608acdc9366e1af4107ce Mon Sep 17 00:00:00 2001 From: Matthew Austew Date: Wed, 27 May 2026 11:55:23 -0500 Subject: [PATCH 06/11] This should install Carton only when Perl is implicated on either OS. --- .../actions/setup-runtimes-caching/action.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/actions/setup-runtimes-caching/action.yml b/.github/actions/setup-runtimes-caching/action.yml index e72dc8ba1..42f110853 100644 --- a/.github/actions/setup-runtimes-caching/action.yml +++ b/.github/actions/setup-runtimes-caching/action.yml @@ -105,6 +105,22 @@ runs: shell: bash run: sudo apt-get install -y cpanminus + - name: Install Carton on Linux + if: ${{ runner.os == 'Linux' && contains(inputs.name, 'Hosting.Perl') }} + shell: bash + run: sudo cpanm Carton --notest --force + + - name: Install Carton on Windows + if: ${{ runner.os == 'Windows' && contains(inputs.name, 'Hosting.Perl') }} + shell: pwsh + run: | + $cpanm = Get-Command cpanm -ErrorAction SilentlyContinue + if ($null -eq $cpanm) { + throw 'cpanm was not found on PATH. Expected Strawberry Perl to provide it on the GitHub runner.' + } + + cpanm Carton --notest --force + - uses: actions/cache@v4 name: Cache NuGet packages with: From 1dc42eb5532ee51deddff48d1ff02e1a88e9d8f2 Mon Sep 17 00:00:00 2001 From: Matthew Austew Date: Thu, 28 May 2026 00:14:15 -0500 Subject: [PATCH 07/11] Addressing feedback. --- .../actions/setup-runtimes-caching/action.yml | 16 - eng/testing/validate-typescript-apphost.ps1 | 320 +++--------------- .../apphost.ts | 21 +- .../perl/cpanm-api-integration/scripts/API.pl | 7 - .../PerlAppResourceBuilderExtensions.cs | 8 +- .../AddPerlExecutableTests.cs | 20 -- .../TypeScriptAppHostTests.cs | 27 +- .../WithCertificateTrustTests.cs | 20 -- .../TypeScriptAppHostTest.cs | 46 --- 9 files changed, 70 insertions(+), 415 deletions(-) diff --git a/.github/actions/setup-runtimes-caching/action.yml b/.github/actions/setup-runtimes-caching/action.yml index 42f110853..e72dc8ba1 100644 --- a/.github/actions/setup-runtimes-caching/action.yml +++ b/.github/actions/setup-runtimes-caching/action.yml @@ -105,22 +105,6 @@ runs: shell: bash run: sudo apt-get install -y cpanminus - - name: Install Carton on Linux - if: ${{ runner.os == 'Linux' && contains(inputs.name, 'Hosting.Perl') }} - shell: bash - run: sudo cpanm Carton --notest --force - - - name: Install Carton on Windows - if: ${{ runner.os == 'Windows' && contains(inputs.name, 'Hosting.Perl') }} - shell: pwsh - run: | - $cpanm = Get-Command cpanm -ErrorAction SilentlyContinue - if ($null -eq $cpanm) { - throw 'cpanm was not found on PATH. Expected Strawberry Perl to provide it on the GitHub runner.' - } - - cpanm Carton --notest --force - - uses: actions/cache@v4 name: Cache NuGet packages with: diff --git a/eng/testing/validate-typescript-apphost.ps1 b/eng/testing/validate-typescript-apphost.ps1 index 759589e39..36c74e9d2 100644 --- a/eng/testing/validate-typescript-apphost.ps1 +++ b/eng/testing/validate-typescript-apphost.ps1 @@ -4,15 +4,12 @@ param( [Parameter(Mandatory = $true)] [string]$AppHostPath, - [string]$PackageProjectPath = "", + [Parameter(Mandatory = $true)] + [string]$PackageProjectPath, [Parameter(Mandatory = $true)] [string]$PackageName, - [switch]$UseConfiguredPackages, - - [switch]$SkipStart, - [string[]]$WaitForResources = @(), [string[]]$RequiredCommands = @(), @@ -24,15 +21,6 @@ param( [int]$WaitTimeoutSeconds = 180, - [string]$HttpProbeResource = "", - - [string]$HttpProbeEndpointName = "http", - - [string]$HttpProbePath = "", - - [AllowEmptyString()] - [string]$HttpProbeExpectedText, - [string[]]$Secrets = @() ) @@ -80,27 +68,6 @@ function Invoke-ExternalCommand { } } -function Invoke-ExternalCommandCaptureOutput { - param( - [Parameter(Mandatory = $true)] - [string]$FilePath, - - [Parameter(Mandatory = $true)] - [string[]]$Arguments - ) - - $resolvedFilePath = Resolve-ExternalCommandPath $FilePath - $output = & $resolvedFilePath @Arguments 2>&1 - $outputText = (($output | ForEach-Object { $_.ToString() }) -join [Environment]::NewLine) - - if ($LASTEXITCODE -ne 0) { - $joinedArguments = [string]::Join(" ", $Arguments) - throw "Command failed with exit code ${LASTEXITCODE}: $FilePath $joinedArguments" - } - - return $outputText -} - function Invoke-CleanupStep { param( [Parameter(Mandatory = $true)] @@ -160,170 +127,37 @@ function Remove-PathWithRetry { } } -function ConvertFrom-AspireJsonOutput { - param( - [Parameter(Mandatory = $true)] - [string]$Text - ) - - $jsonStartIndex = $Text.IndexOf('{') - while ($jsonStartIndex -ge 0) { - $depth = 0 - $inString = $false - $isEscaped = $false - - for ($index = $jsonStartIndex; $index -lt $Text.Length; $index++) { - $character = $Text[$index] - - if ($isEscaped) { - $isEscaped = $false - continue - } - - if ($character -eq '\\') { - if ($inString) { - $isEscaped = $true - } - - continue - } - - if ($character -eq '"') { - $inString = -not $inString - continue - } - - if ($inString) { - continue - } - - if ($character -eq '{') { - $depth++ - continue - } - - if ($character -eq '}') { - $depth-- - if ($depth -eq 0) { - $jsonText = $Text.Substring($jsonStartIndex, ($index - $jsonStartIndex) + 1) - try { - return $jsonText | ConvertFrom-Json -AsHashtable -Depth 100 - } - catch { - break - } - } - } - } - - $jsonStartIndex = $Text.IndexOf('{', $jsonStartIndex + 1) - } - - throw "Could not find a JSON payload in Aspire command output." -} - -function Get-ResourceEndpointUrl { - param( - [Parameter(Mandatory = $true)] - [hashtable]$DescribePayload, - - [Parameter(Mandatory = $true)] - [string]$ResourceName, - - [Parameter(Mandatory = $true)] - [string]$EndpointName - ) - - $resource = @($DescribePayload["resources"]) | - Where-Object { - $_["displayName"] -eq $ResourceName -or $_["name"] -eq $ResourceName - } | - Select-Object -First 1 - - if ($null -eq $resource) { - throw "Resource '$ResourceName' was not present in 'aspire describe' output." - } - - $endpoint = @($resource["urls"]) | - Where-Object { $_["name"] -eq $EndpointName } | - Select-Object -First 1 - - if ($null -eq $endpoint -or [string]::IsNullOrWhiteSpace($endpoint["url"])) { - throw "Resource '$ResourceName' did not expose endpoint '$EndpointName' in 'aspire describe' output." - } - - return $endpoint["url"] -} - -function Assert-HttpProbeResponse { - param( - [Parameter(Mandatory = $true)] - [string]$BaseUrl, - - [Parameter(Mandatory = $true)] - [string]$Path, - - [Parameter(Mandatory = $true)] - [AllowEmptyString()] - [string]$ExpectedText - ) - - $probeUri = [System.Uri]::new([System.Uri]$BaseUrl, $Path) - $response = Invoke-WebRequest -Uri $probeUri -TimeoutSec 30 - $actualText = $response.Content - - if ($actualText -ne $ExpectedText) { - throw "HTTP probe for '$probeUri' returned '$actualText' instead of expected '$ExpectedText'." - } -} - $resolvedAppHostPath = (Resolve-Path $AppHostPath).Path -$resolvedPackageProjectPath = $null -$localSource = $null - -if (-not $UseConfiguredPackages) { - if ([string]::IsNullOrWhiteSpace($PackageProjectPath)) { - throw "PackageProjectPath is required unless -UseConfiguredPackages is set." - } - - $resolvedPackageProjectPath = (Resolve-Path $PackageProjectPath).Path -} - +$resolvedPackageProjectPath = (Resolve-Path $PackageProjectPath).Path $appHostDirectory = Split-Path -Parent $resolvedAppHostPath +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\\..")).Path $configPath = Join-Path $appHostDirectory "aspire.config.json" $nugetConfigPath = Join-Path $appHostDirectory "nuget.config" -$generatedRestoreRoot = Join-Path $appHostDirectory ".aspire\integrations\package-restore" -$generatedRestoreNugetConfigPath = Join-Path $generatedRestoreRoot "nuget.config" +$localSource = Join-Path ([System.IO.Path]::GetTempPath()) ("ct-polyglot-" + [Guid]::NewGuid().ToString("N")) $originalConfig = $null $appStarted = $false $primaryError = $null $cleanupFailures = [System.Collections.Generic.List[string]]::new() -if (-not $UseConfiguredPackages) { - $localSource = Join-Path ([System.IO.Path]::GetTempPath()) ("ct-polyglot-" + [Guid]::NewGuid().ToString("N")) - - if ([string]::IsNullOrWhiteSpace($PackageVersion)) { - $versionPrefix = (& dotnet msbuild $resolvedPackageProjectPath -nologo -v:q -getProperty:VersionPrefix).Trim() - if ([string]::IsNullOrWhiteSpace($versionPrefix)) { - throw "Could not determine the evaluated VersionPrefix for $resolvedPackageProjectPath." - } - - $PackageVersion = "$versionPrefix-polyglot.local" +if ([string]::IsNullOrWhiteSpace($PackageVersion)) { + $versionPrefix = (& dotnet msbuild $resolvedPackageProjectPath -nologo -v:q -getProperty:VersionPrefix).Trim() + if ([string]::IsNullOrWhiteSpace($versionPrefix)) { + throw "Could not determine the evaluated VersionPrefix for $resolvedPackageProjectPath." } + + $PackageVersion = "$versionPrefix-polyglot.local" } # Discover local CommunityToolkit project references that also need packing $localDependencies = @() -if (-not $UseConfiguredPackages) { - $projRefJson = (& dotnet msbuild $resolvedPackageProjectPath -nologo -v:q -getItem:ProjectReference) | Out-String - $projRefData = $projRefJson | ConvertFrom-Json - $projRefs = @($projRefData.Items.ProjectReference) - foreach ($ref in $projRefs) { - if ($ref.Filename -like "CommunityToolkit.*") { - $localDependencies += @{ - Name = $ref.Filename - FullPath = $ref.FullPath - } +$projRefJson = (& dotnet msbuild $resolvedPackageProjectPath -nologo -v:q -getItem:ProjectReference) | Out-String +$projRefData = $projRefJson | ConvertFrom-Json +$projRefs = @($projRefData.Items.ProjectReference) +foreach ($ref in $projRefs) { + if ($ref.Filename -like "CommunityToolkit.*") { + $localDependencies += @{ + Name = $ref.Filename + FullPath = $ref.FullPath } } } @@ -377,82 +211,53 @@ foreach ($secret in $Secrets) { $parsedSecrets.Add(@($key, $value)) } -$hasHttpProbe = - -not [string]::IsNullOrWhiteSpace($HttpProbeResource) -or - -not [string]::IsNullOrWhiteSpace($HttpProbePath) -or - $PSBoundParameters.ContainsKey("HttpProbeExpectedText") - -if ($hasHttpProbe -and ( - [string]::IsNullOrWhiteSpace($HttpProbeResource) -or - [string]::IsNullOrWhiteSpace($HttpProbePath) -or - -not $PSBoundParameters.ContainsKey("HttpProbeExpectedText"))) { - throw "HttpProbeResource, HttpProbePath, and HttpProbeExpectedText must all be provided together." -} - -if ($hasHttpProbe -and [string]::IsNullOrWhiteSpace($HttpProbeEndpointName)) { - throw "HttpProbeEndpointName cannot be empty when HTTP probing is enabled." -} - try { $originalConfig = Get-Content -Path $configPath -Raw - if (-not $UseConfiguredPackages) { - New-Item -ItemType Directory -Path $localSource -Force | Out-Null + New-Item -ItemType Directory -Path $localSource -Force | Out-Null + + Invoke-ExternalCommand "dotnet" @( + "pack", + $resolvedPackageProjectPath, + "-c", "Debug", + "-p:PackageVersion=$PackageVersion", + "-o", $localSource + ) + foreach ($dep in $localDependencies) { Invoke-ExternalCommand "dotnet" @( "pack", - $resolvedPackageProjectPath, + $dep.FullPath, "-c", "Debug", "-p:PackageVersion=$PackageVersion", "-o", $localSource ) + } - foreach ($dep in $localDependencies) { - Invoke-ExternalCommand "dotnet" @( - "pack", - $dep.FullPath, - "-c", "Debug", - "-p:PackageVersion=$PackageVersion", - "-o", $localSource - ) - } - - $config = $originalConfig | ConvertFrom-Json -AsHashtable - if ($null -eq $config["packages"]) { - $config["packages"] = [ordered]@{} - } + $config = $originalConfig | ConvertFrom-Json -AsHashtable + if ($null -eq $config["packages"]) { + $config["packages"] = [ordered]@{} + } - $config["packages"][$PackageName] = $PackageVersion - foreach ($dep in $localDependencies) { - $config["packages"][$dep.Name] = $PackageVersion - } - $config | ConvertTo-Json -Depth 10 | Set-Content -Path $configPath -NoNewline + $config["packages"][$PackageName] = $PackageVersion + foreach ($dep in $localDependencies) { + $config["packages"][$dep.Name] = $PackageVersion + } + $config | ConvertTo-Json -Depth 10 | Set-Content -Path $configPath -NoNewline - $temporaryNugetConfig = @" + @" - - - - - - - - - - - - - - - - + + + + + + + + + -"@ - - $temporaryNugetConfig | Set-Content -Path $nugetConfigPath -NoNewline - New-Item -ItemType Directory -Path $generatedRestoreRoot -Force | Out-Null - $temporaryNugetConfig | Set-Content -Path $generatedRestoreNugetConfigPath -NoNewline - } +"@ | Set-Content -Path $nugetConfigPath -NoNewline Push-Location $appHostDirectory try { @@ -464,10 +269,6 @@ try { "--log-level", "debug" ) Invoke-ExternalCommand "npx" @("tsc", "--noEmit") - - if ($SkipStart) { - return - } } finally { Pop-Location @@ -506,20 +307,12 @@ try { ) } - $describeOutput = Invoke-ExternalCommandCaptureOutput "aspire" @( + Invoke-ExternalCommand "aspire" @( "describe", "--apphost", $resolvedAppHostPath, "--format", "Json", - "--log-level", "warning" + "--log-level", "debug" ) - - Write-Output $describeOutput - - if ($hasHttpProbe) { - $describePayload = ConvertFrom-AspireJsonOutput $describeOutput - $endpointUrl = Get-ResourceEndpointUrl -DescribePayload $describePayload -ResourceName $HttpProbeResource -EndpointName $HttpProbeEndpointName - Assert-HttpProbeResponse -BaseUrl $endpointUrl -Path $HttpProbePath -ExpectedText $HttpProbeExpectedText - } } finally { Pop-Location @@ -565,13 +358,10 @@ finally { Invoke-CleanupStep -Description "remove generated nuget.config" -Action { Remove-PathWithRetry -Path $nugetConfigPath - Remove-PathWithRetry -Path $generatedRestoreNugetConfigPath } -Failures $cleanupFailures Invoke-CleanupStep -Description "remove local package source" -Action { - if (-not [string]::IsNullOrWhiteSpace($localSource)) { - Remove-PathWithRetry -Path $localSource -Recurse - } + Remove-PathWithRetry -Path $localSource -Recurse } -Failures $cleanupFailures } @@ -587,4 +377,4 @@ if ($cleanupFailures.Count -gt 0) { if ($null -ne $primaryError) { throw $primaryError -} +} \ No newline at end of file diff --git a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/apphost.ts b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/apphost.ts index 2a7e9650a..251fd1ed9 100644 --- a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/apphost.ts +++ b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/apphost.ts @@ -1,19 +1,16 @@ -import { CertificateTrustScope, createBuilder } from "./.modules/aspire.js"; +import { createBuilder } from "./.modules/aspire.js"; const builder = await createBuilder(); -const cartonProjectApi = await builder.addPerlApi("perl-api", "../scripts", "API.pl"); -await cartonProjectApi.withCarton(); -await cartonProjectApi.withProjectDependencies({ cartonDeployment: true }); -await cartonProjectApi.withLocalLib({ path: "local" }); -await cartonProjectApi.withDeveloperCertificateTrust(true); -await cartonProjectApi.withCertificateTrustScope(CertificateTrustScope.Append); -await cartonProjectApi.withPerlCertificateTrust(); -await cartonProjectApi.withHttpEndpoint({ name: "http", env: "PORT" }); +const perlApi = await builder.addPerlApi("perl-api", ".", "../scripts/API.pl"); +await perlApi.withCpanMinus(); +await perlApi.withPackage("Mojolicious::Lite", { force: true, skipTest: true }); +await perlApi.withLocalLib({ path: "local" }); +await perlApi.withHttpEndpoint({ name: "http", env: "PORT" }); const perlDriver = await builder.addPerlScript("perl-driver", "../scripts", "driver.pl"); -await perlDriver.withEnvironment("API_URL", cartonProjectApi.getEndpoint("http")); -await perlDriver.withReference(cartonProjectApi); -await perlDriver.waitFor(cartonProjectApi); +await perlDriver.withEnvironment("API_URL", perlApi.getEndpoint("http")); +await perlDriver.withReference(perlApi); +await perlDriver.waitFor(perlApi); await builder.build().run(); \ No newline at end of file diff --git a/examples/perl/cpanm-api-integration/scripts/API.pl b/examples/perl/cpanm-api-integration/scripts/API.pl index c54f9431f..ce88f2d43 100644 --- a/examples/perl/cpanm-api-integration/scripts/API.pl +++ b/examples/perl/cpanm-api-integration/scripts/API.pl @@ -12,11 +12,4 @@ $c->render(text => 'fragile'); }; -get '/cert-env' => sub ($c) { - my @required = qw(SSL_CERT_FILE PERL_LWP_SSL_CA_FILE MOJO_CA_FILE); - my $has_all = !grep { !defined $ENV{$_} || $ENV{$_} eq '' } @required; - - $c->render(text => $has_all ? 'present' : 'missing'); -}; - app->start('daemon', map { ('-l', $_) } @listeners); diff --git a/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.cs index 50acaf2b7..829ebfb87 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.cs @@ -214,12 +214,8 @@ private static IResourceBuilder AddPerlAppCore( } resourceBuilder.WithOtlpExporter(); - - if (entrypointType != EntrypointType.Executable) - { - resourceBuilder.WithRequiredCommand("perl", "https://www.perl.org/get.html"); - resourceBuilder.WithRequiredCommand("cpan", "https://metacpan.org/pod/CPAN"); - } + resourceBuilder.WithRequiredCommand("perl", "https://www.perl.org/get.html"); + resourceBuilder.WithRequiredCommand("cpan", "https://metacpan.org/pod/CPAN"); // Configure OpenTelemetry exporters using environment variables // https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#exporter-selection diff --git a/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/AddPerlExecutableTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/AddPerlExecutableTests.cs index 60d1546f2..405b14ab0 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/AddPerlExecutableTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/AddPerlExecutableTests.cs @@ -37,24 +37,4 @@ public void AddPerlExecutableShouldThrowWhenBuilderIsNull() Assert.Throws(() => builder.AddPerlExecutable("perl-bin", "bin", "my-compiled-perl")); } - - [Fact] - public void AddPerlExecutable_DoesNotRegisterInterpreterRequiredCommands() - { - var builder = DistributedApplication.CreateBuilder(); - - builder.AddPerlExecutable("perl-bin", "bin", "my-compiled-perl"); - - using var app = builder.Build(); - - var appModel = app.Services.GetRequiredService(); - var resource = Assert.Single(appModel.Resources.OfType()); - -#pragma warning disable ASPIRECOMMAND001 - var requiredCommands = resource.Annotations.OfType().ToList(); -#pragma warning restore ASPIRECOMMAND001 - - Assert.DoesNotContain(requiredCommands, annotation => annotation.Command == "perl"); - Assert.DoesNotContain(requiredCommands, annotation => annotation.Command == "cpan"); - } } diff --git a/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs index 6deecae2e..3c93f4abd 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/TypeScriptAppHostTests.cs @@ -5,34 +5,15 @@ namespace CommunityToolkit.Aspire.Hosting.Perl.Tests; public class TypeScriptAppHostTests { [Fact] - public async Task TypeScriptAppHostCartonProjectDependencyScenario_FleetingEndpoint_ReturnsExpectedText() + public async Task TypeScriptAppHostCompilesAndStarts() { await TypeScriptAppHostTest.Run( appHostProject: "CpanmApiIntegration.AppHost.TypeScript", packageName: "CommunityToolkit.Aspire.Hosting.Perl", exampleName: "perl/cpanm-api-integration", - waitForResources: ["perl-api", "perl-driver"], - requiredCommands: ["perl", "carton"], - useConfiguredPackages: true, - httpProbeResource: "perl-api", - httpProbePath: "/fleeting", - httpProbeExpectedText: "fragile", - cancellationToken: TestContext.Current.CancellationToken); - } - - [Fact] - public async Task TypeScriptAppHostCartonProjectDependencyScenario_CertificateTrustEnv_ReturnsPresent() - { - await TypeScriptAppHostTest.Run( - appHostProject: "CpanmApiIntegration.AppHost.TypeScript", - packageName: "CommunityToolkit.Aspire.Hosting.Perl", - exampleName: "perl/cpanm-api-integration", - waitForResources: ["perl-api", "perl-driver"], - requiredCommands: ["perl", "carton"], - useConfiguredPackages: true, - httpProbeResource: "perl-api", - httpProbePath: "/cert-env", - httpProbeExpectedText: "present", + waitForResources: ["perl-api"], + waitStatus: "up", + requiredCommands: ["perl", "cpanm"], cancellationToken: TestContext.Current.CancellationToken); } } \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/WithCertificateTrustTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/WithCertificateTrustTests.cs index dc8c57d26..7602dcde8 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/WithCertificateTrustTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Perl.Tests/WithCertificateTrustTests.cs @@ -108,24 +108,4 @@ public void WithPerlCertificateTrust_BeforeWithPackage_StillPropagates() // WithPerlCertificateTrust() was called before WithPackage() Assert.Single(installerResource.Annotations.OfType()); } - - [Fact] - public void WithPerlCertificateTrust_WithProjectDependencies_PropagatesToProjectInstaller() - { - var builder = DistributedApplication.CreateBuilder(); - -#pragma warning disable CTASPIREPERL001 - builder.AddPerlScript("perl-app", "scripts", "app.pl") - .WithProjectDependencies() - .WithPerlCertificateTrust(); -#pragma warning restore CTASPIREPERL001 - - using var app = builder.Build(); - - var appModel = app.Services.GetRequiredService(); - var installerResource = Assert.Single(appModel.Resources.OfType()); - - Assert.Single(installerResource.Annotations.OfType()); - Assert.Single(installerResource.Annotations.OfType()); - } } diff --git a/tests/CommunityToolkit.Aspire.Testing/TypeScriptAppHostTest.cs b/tests/CommunityToolkit.Aspire.Testing/TypeScriptAppHostTest.cs index d0f9a4256..350e1f77d 100644 --- a/tests/CommunityToolkit.Aspire.Testing/TypeScriptAppHostTest.cs +++ b/tests/CommunityToolkit.Aspire.Testing/TypeScriptAppHostTest.cs @@ -15,12 +15,7 @@ public static class TypeScriptAppHostTest /// The Aspire resource status to wait for. /// Optional commands that must exist on PATH before validation runs. /// to validate the AppHost using the package mappings already present in aspire.config.json instead of packing a local polyglot package first. - /// Optional resource display name to probe over HTTP after startup validation completes. - /// Optional relative path to request from the probed HTTP endpoint. - /// Optional exact response text expected from the HTTP probe. - /// The named endpoint to probe when is provided. /// Optional dictionary of secret key-value pairs to set via aspire secret set before starting the app host. - /// to stop after restore and type-check validation without starting the AppHost. /// Optional repository-relative path to an apphost.ts file. When omitted, the path is resolved from and . /// The cancellation token. public static async Task Run( @@ -31,12 +26,7 @@ public static async Task Run( string waitStatus = "healthy", IEnumerable? requiredCommands = null, bool useConfiguredPackages = false, - string? httpProbeResource = null, - string? httpProbePath = null, - string? httpProbeExpectedText = null, - string httpProbeEndpointName = "http", Dictionary? secrets = null, - bool skipStart = false, string? appHostPathRelativeToRepo = null, CancellationToken cancellationToken = default) { @@ -50,25 +40,6 @@ public static async Task Run( throw new ArgumentException("Wait status must be one of 'healthy', 'up', or 'down'.", nameof(waitStatus)); } - bool hasHttpProbeConfiguration = - !string.IsNullOrWhiteSpace(httpProbeResource) - || !string.IsNullOrWhiteSpace(httpProbePath) - || httpProbeExpectedText is not null; - - if (hasHttpProbeConfiguration && - (string.IsNullOrWhiteSpace(httpProbeResource) - || string.IsNullOrWhiteSpace(httpProbePath) - || httpProbeExpectedText is null)) - { - throw new ArgumentException( - "HTTP probing requires a resource name, request path, and expected response text."); - } - - if (hasHttpProbeConfiguration && string.IsNullOrWhiteSpace(httpProbeEndpointName)) - { - throw new ArgumentException("HTTP probe endpoint name cannot be empty.", nameof(httpProbeEndpointName)); - } - List resources = waitForResources .Where(static resource => !string.IsNullOrWhiteSpace(resource)) .ToList(); @@ -118,23 +89,6 @@ public static async Task Run( arguments.Add("-UseConfiguredPackages"); } - if (skipStart) - { - arguments.Add("-SkipStart"); - } - - if (hasHttpProbeConfiguration) - { - arguments.Add("-HttpProbeResource"); - arguments.Add(httpProbeResource!); - arguments.Add("-HttpProbePath"); - arguments.Add(httpProbePath!); - arguments.Add("-HttpProbeExpectedText"); - arguments.Add(httpProbeExpectedText!); - arguments.Add("-HttpProbeEndpointName"); - arguments.Add(httpProbeEndpointName); - } - if (secrets is { Count: > 0 }) { arguments.Add("-Secrets"); From 0c996ec6dd1a66485718fca1dac58d627a7bfecf Mon Sep 17 00:00:00 2001 From: Matthew Austew Date: Thu, 28 May 2026 21:26:09 -0500 Subject: [PATCH 08/11] Okay perl and Bun are th two integrations affected by this. The offense is that "channel: stable" is blocking the polyglot.local packages from being picked up _UNLESS_ you have already built it and it's in your cache. This sent me down a rabbit hole that made me work around it with the powershell tweaks. --- .../CpanmApiIntegration.AppHost.TypeScript/aspire.config.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/aspire.config.json b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/aspire.config.json index e402cd952..5aa75514a 100644 --- a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/aspire.config.json +++ b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/aspire.config.json @@ -6,7 +6,6 @@ "sdk": { "version": "13.3.0" }, - "channel": "stable", "profiles": { "https": { "applicationUrl": "https://localhost:17453;http://localhost:15453", @@ -17,6 +16,6 @@ } }, "packages": { - "CommunityToolkit.Aspire.Hosting.Perl": "../../../../src/CommunityToolkit.Aspire.Hosting.Perl/CommunityToolkit.Aspire.Hosting.Perl.csproj" + "CommunityToolkit.Aspire.Hosting.Perl": "13.3.1-polyglot.local" } } \ No newline at end of file From 1578403d40b3d0bf4f5f560f8d9b012c556d473d Mon Sep 17 00:00:00 2001 From: Omnideth Date: Sat, 30 May 2026 16:07:04 -0500 Subject: [PATCH 09/11] As main has shifted under me, it looks like this makes me congruent with the current state of .mts Typescript implementations. I am less confident about stripping down the AspireExport() and removing the description, but I believe Seb did this as part of PR 1370. --- .../{apphost.ts => apphost.mts} | 2 +- .../aspire.config.json | 6 +++--- .../eslint.config.mjs | 2 +- .../package-lock.json | 5 ----- .../package.json | 2 +- .../tsconfig.json | 4 ++-- ...ppResourceBuilderExtensions.PackageManager.cs | 12 ++---------- .../PerlAppResourceBuilderExtensions.cs | 16 ++++++++-------- 8 files changed, 18 insertions(+), 31 deletions(-) rename examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/{apphost.ts => apphost.mts} (90%) diff --git a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/apphost.ts b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/apphost.mts similarity index 90% rename from examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/apphost.ts rename to examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/apphost.mts index 251fd1ed9..c28d41800 100644 --- a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/apphost.ts +++ b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/apphost.mts @@ -1,4 +1,4 @@ -import { createBuilder } from "./.modules/aspire.js"; +import { createBuilder } from "./.aspire/modules/aspire.mjs"; const builder = await createBuilder(); diff --git a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/aspire.config.json b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/aspire.config.json index 5aa75514a..9da63f124 100644 --- a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/aspire.config.json +++ b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/aspire.config.json @@ -1,10 +1,10 @@ { "appHost": { - "path": "apphost.ts", + "path": "apphost.mts", "language": "typescript/nodejs" }, "sdk": { - "version": "13.3.0" + "version": "13.4.0-preview.1.26275.15" }, "profiles": { "https": { @@ -16,6 +16,6 @@ } }, "packages": { - "CommunityToolkit.Aspire.Hosting.Perl": "13.3.1-polyglot.local" + "CommunityToolkit.Aspire.Hosting.Perl": "../../../../src/CommunityToolkit.Aspire.Hosting.Perl/CommunityToolkit.Aspire.Hosting.Perl.csproj" } } \ No newline at end of file diff --git a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/eslint.config.mjs b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/eslint.config.mjs index 73a2cb305..b1503919e 100644 --- a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/eslint.config.mjs +++ b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/eslint.config.mjs @@ -4,7 +4,7 @@ import { defineConfig } from "eslint/config"; import tseslint from "typescript-eslint"; export default defineConfig({ - files: ["apphost.ts"], + files: ["apphost.mts"], extends: [tseslint.configs.base], languageOptions: { parserOptions: { diff --git a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/package-lock.json b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/package-lock.json index b54a33af7..5691f71de 100644 --- a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/package-lock.json +++ b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/package-lock.json @@ -713,7 +713,6 @@ "integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.59.4", "@typescript-eslint/types": "8.59.4", @@ -905,7 +904,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1142,7 +1140,6 @@ "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -1848,7 +1845,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1930,7 +1926,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/package.json b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/package.json index b67178aee..1ff404d7a 100644 --- a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/package.json +++ b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "scripts": { - "lint": "eslint apphost.ts", + "lint": "eslint apphost.mts", "predev": "npm run lint", "dev": "aspire run", "prebuild": "npm run lint", diff --git a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/tsconfig.json b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/tsconfig.json index 8b748a15a..c34e4c0b3 100644 --- a/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/tsconfig.json +++ b/examples/perl/cpanm-api-integration/CpanmApiIntegration.AppHost.TypeScript/tsconfig.json @@ -11,8 +11,8 @@ "rootDir": "." }, "include": [ - "apphost.ts", - ".modules/**/*.ts" + "apphost.mts", + ".aspire/modules/**/*.ts" ], "exclude": [ "node_modules" diff --git a/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.PackageManager.cs b/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.PackageManager.cs index 022b63c58..637786c8e 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.PackageManager.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.PackageManager.cs @@ -21,7 +21,7 @@ public static partial class PerlAppResourceBuilderExtensions /// The type of the Perl application resource. /// The resource builder. /// A reference to the . - [AspireExport("withCpanMinus", Description = "Configures the Perl resource to use cpanm for package installation")] + [AspireExport] public static IResourceBuilder WithCpanMinus( this IResourceBuilder builder) where TResource : PerlAppResource { @@ -169,11 +169,7 @@ internal static bool IsCommandAvailable(string resolvedPath, string? pathValue, /// The resource builder. /// A reference to the . /// -<<<<<<< HEAD [AspireExport] -======= - [AspireExport("withCarton", Description = "Configures the Perl resource to use Carton for project dependency installation")] ->>>>>>> e954eeb2 (Committing these to pivot boxes, this is steps 11-13, I do have a question about if these validate-typescript-apphost.ps1 changes are required. Will interrogate later.) public static IResourceBuilder WithCarton( this IResourceBuilder builder) where TResource : PerlAppResource { @@ -216,7 +212,7 @@ public static IResourceBuilder WithCarton( /// cpan by default, or cpanm if /// was called. /// - [AspireExport("withPackage", Description = "Installs a Perl package before the resource starts")] + [AspireExport] public static IResourceBuilder WithPackage( this IResourceBuilder builder, string packageName, @@ -262,11 +258,7 @@ public static IResourceBuilder WithPackage( /// When using Carton with set to true, /// a cpanfile.snapshot must also be present. /// -<<<<<<< HEAD [AspireExport] -======= - [AspireExport("withProjectDependencies", Description = "Installs Perl project dependencies before the resource starts")] ->>>>>>> e954eeb2 (Committing these to pivot boxes, this is steps 11-13, I do have a question about if these validate-typescript-apphost.ps1 changes are required. Will interrogate later.) public static IResourceBuilder WithProjectDependencies( this IResourceBuilder builder, bool cartonDeployment = false) where TResource : PerlAppResource diff --git a/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.cs index 829ebfb87..262577cd5 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Perl/PerlAppResourceBuilderExtensions.cs @@ -49,7 +49,7 @@ public static partial class PerlAppResourceBuilderExtensions /// builder.Build().Run(); /// /// - [AspireExport("addPerlScript", Description = "Adds a Perl script resource")] + [AspireExport] public static IResourceBuilder AddPerlScript( this IDistributedApplicationBuilder builder, [ResourceName] string resourceName, string appDirectory, string scriptName) => AddPerlAppCore(builder, resourceName, appDirectory, EntrypointType.Script, scriptName, DefaultPerlEnvironment); @@ -89,7 +89,7 @@ public static IResourceBuilder AddPerlScript( /// builder.Build().Run(); /// /// - [AspireExport("addPerlApi", Description = "Adds a Perl API resource")] + [AspireExport] public static IResourceBuilder AddPerlApi( this IDistributedApplicationBuilder builder, [ResourceName] string resourceName, string appDirectory, string scriptName) => AddPerlAppCore(builder, resourceName, appDirectory, EntrypointType.API, scriptName, DefaultPerlEnvironment, "daemon"); @@ -110,7 +110,7 @@ public static IResourceBuilder AddPerlApi( /// builder.Build().Run(); /// /// - [AspireExport("addPerlModule", Description = "Adds a Perl module resource")] + [AspireExport] public static IResourceBuilder AddPerlModule( this IDistributedApplicationBuilder builder, [ResourceName] string resourceName, string appDirectory, string moduleName) => AddPerlAppCore(builder, resourceName, appDirectory, EntrypointType.Module, moduleName, DefaultPerlEnvironment); @@ -131,7 +131,7 @@ public static IResourceBuilder AddPerlModule( /// builder.Build().Run(); /// /// - [AspireExport("addPerlExecutable", Description = "Adds a Perl executable resource")] + [AspireExport] public static IResourceBuilder AddPerlExecutable( this IDistributedApplicationBuilder builder, [ResourceName] string resourceName, string appDirectory, string executablePath) => AddPerlAppCore(builder, resourceName, appDirectory, EntrypointType.Executable, executablePath, DefaultPerlEnvironment); @@ -325,7 +325,7 @@ private static void AddWaitIfMissing(IResource targetResource, IResource depende /// the PERLBREW_ROOT environment variable, or defaults to ~/perl5/perlbrew. /// /// A reference to the . - [AspireExport("withPerlbrew", Description = "Configures the Perl resource to use a perlbrew-managed Perl version")] + [AspireExport] public static IResourceBuilder WithPerlbrew( this IResourceBuilder builder, string version, string? perlbrewRoot = null) where T : PerlAppResource => builder.WithPerlbrewEnvironment(version, perlbrewRoot); @@ -356,7 +356,7 @@ public static IResourceBuilder WithPerlbrew( /// and enabling per-project module isolation. /// /// - [AspireExport("withPerlbrewEnvironment", Description = "Configures the Perl resource to use a perlbrew-managed Perl version and environment")] + [AspireExport] public static IResourceBuilder WithPerlbrewEnvironment( this IResourceBuilder builder, string version, string? perlbrewRoot = null) where T : PerlAppResource { @@ -465,7 +465,7 @@ await interactionService.PromptNotificationAsync( /// While it is possible to use cpan with Local::Lib it is complicated to model in Aspire. /// /// - [AspireExport("withLocalLib", Description = "Configures a local::lib directory for Perl module isolation")] + [AspireExport] public static IResourceBuilder WithLocalLib( this IResourceBuilder builder, string path = "local") where TResource : PerlAppResource @@ -501,7 +501,7 @@ public static IResourceBuilder WithLocalLib( /// The resource builder. /// A reference to the . [Experimental("CTASPIREPERL001")] - [AspireExport("withPerlCertificateTrust", Description = "Configures Perl certificate trust using Aspire-provided certificate bundle settings")] + [AspireExport] public static IResourceBuilder WithPerlCertificateTrust( this IResourceBuilder builder) where TResource : PerlAppResource { From 57e94aae80cea7027202508e514fc0bdd8fed03a Mon Sep 17 00:00:00 2001 From: Omnideth Date: Sat, 30 May 2026 16:17:25 -0500 Subject: [PATCH 10/11] I am reverting the apphost.ps1 changes from before, they made their way back in. These are no longer needed simply because I stopped using "channel: stable" in the aspire.config.json. --- eng/testing/validate-typescript-apphost.ps1 | 137 -------------------- 1 file changed, 137 deletions(-) diff --git a/eng/testing/validate-typescript-apphost.ps1 b/eng/testing/validate-typescript-apphost.ps1 index 36c74e9d2..3ad7b7387 100644 --- a/eng/testing/validate-typescript-apphost.ps1 +++ b/eng/testing/validate-typescript-apphost.ps1 @@ -4,18 +4,10 @@ param( [Parameter(Mandatory = $true)] [string]$AppHostPath, - [Parameter(Mandatory = $true)] - [string]$PackageProjectPath, - - [Parameter(Mandatory = $true)] - [string]$PackageName, - [string[]]$WaitForResources = @(), [string[]]$RequiredCommands = @(), - [string]$PackageVersion = "", - [ValidateSet("healthy", "up", "down")] [string]$WaitStatus = "healthy", @@ -93,80 +85,12 @@ function Invoke-CleanupStep { } } -function Remove-PathWithRetry { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - - [switch]$Recurse - ) - - if (-not (Test-Path -LiteralPath $Path)) { - return - } - - $maxAttempts = 6 - for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) { - try { - if ($Recurse) { - Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction Stop - } - else { - Remove-Item -LiteralPath $Path -Force -ErrorAction Stop - } - - return - } - catch { - if ($attempt -eq $maxAttempts) { - throw - } - - Start-Sleep -Milliseconds (250 * $attempt) - } - } -} - $resolvedAppHostPath = (Resolve-Path $AppHostPath).Path -$resolvedPackageProjectPath = (Resolve-Path $PackageProjectPath).Path $appHostDirectory = Split-Path -Parent $resolvedAppHostPath -$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\\..")).Path -$configPath = Join-Path $appHostDirectory "aspire.config.json" -$nugetConfigPath = Join-Path $appHostDirectory "nuget.config" -$localSource = Join-Path ([System.IO.Path]::GetTempPath()) ("ct-polyglot-" + [Guid]::NewGuid().ToString("N")) -$originalConfig = $null $appStarted = $false $primaryError = $null $cleanupFailures = [System.Collections.Generic.List[string]]::new() -if ([string]::IsNullOrWhiteSpace($PackageVersion)) { - $versionPrefix = (& dotnet msbuild $resolvedPackageProjectPath -nologo -v:q -getProperty:VersionPrefix).Trim() - if ([string]::IsNullOrWhiteSpace($versionPrefix)) { - throw "Could not determine the evaluated VersionPrefix for $resolvedPackageProjectPath." - } - - $PackageVersion = "$versionPrefix-polyglot.local" -} - -# Discover local CommunityToolkit project references that also need packing -$localDependencies = @() -$projRefJson = (& dotnet msbuild $resolvedPackageProjectPath -nologo -v:q -getItem:ProjectReference) | Out-String -$projRefData = $projRefJson | ConvertFrom-Json -$projRefs = @($projRefData.Items.ProjectReference) -foreach ($ref in $projRefs) { - if ($ref.Filename -like "CommunityToolkit.*") { - $localDependencies += @{ - Name = $ref.Filename - FullPath = $ref.FullPath - } - } -} - -if ($localDependencies.Count -gt 0) { - $depNames = ($localDependencies | ForEach-Object { $_.Name }) -join ", " - Write-Host "Discovered local dependencies to pack: $depNames" -} - if ($WaitForResources.Count -eq 1 -and -not [string]::IsNullOrWhiteSpace($WaitForResources[0])) { $splitOptions = [System.StringSplitOptions]::RemoveEmptyEntries -bor [System.StringSplitOptions]::TrimEntries $WaitForResources = $WaitForResources[0].Split(",", $splitOptions) @@ -212,53 +136,6 @@ foreach ($secret in $Secrets) { } try { - $originalConfig = Get-Content -Path $configPath -Raw - New-Item -ItemType Directory -Path $localSource -Force | Out-Null - - Invoke-ExternalCommand "dotnet" @( - "pack", - $resolvedPackageProjectPath, - "-c", "Debug", - "-p:PackageVersion=$PackageVersion", - "-o", $localSource - ) - - foreach ($dep in $localDependencies) { - Invoke-ExternalCommand "dotnet" @( - "pack", - $dep.FullPath, - "-c", "Debug", - "-p:PackageVersion=$PackageVersion", - "-o", $localSource - ) - } - - $config = $originalConfig | ConvertFrom-Json -AsHashtable - if ($null -eq $config["packages"]) { - $config["packages"] = [ordered]@{} - } - - $config["packages"][$PackageName] = $PackageVersion - foreach ($dep in $localDependencies) { - $config["packages"][$dep.Name] = $PackageVersion - } - $config | ConvertTo-Json -Depth 10 | Set-Content -Path $configPath -NoNewline - - @" - - - - - - - - - - - - -"@ | Set-Content -Path $nugetConfigPath -NoNewline - Push-Location $appHostDirectory try { Invoke-ExternalCommand "npm" @("ci") @@ -349,20 +226,6 @@ finally { ) } } -Failures $cleanupFailures - - Invoke-CleanupStep -Description "restore Aspire config" -Action { - if ($null -ne $originalConfig) { - Set-Content -Path $configPath -Value $originalConfig -NoNewline - } - } -Failures $cleanupFailures - - Invoke-CleanupStep -Description "remove generated nuget.config" -Action { - Remove-PathWithRetry -Path $nugetConfigPath - } -Failures $cleanupFailures - - Invoke-CleanupStep -Description "remove local package source" -Action { - Remove-PathWithRetry -Path $localSource -Recurse - } -Failures $cleanupFailures } if ($cleanupFailures.Count -gt 0) { From 9012eb6c0870aed424b4f3a9c80596a240c911b6 Mon Sep 17 00:00:00 2001 From: Omnideth Date: Sat, 30 May 2026 16:19:59 -0500 Subject: [PATCH 11/11] Add newline at end of validate-typescript-apphost.ps1 I believe this removes the diff hit, I removed the newline after the } --- eng/testing/validate-typescript-apphost.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/testing/validate-typescript-apphost.ps1 b/eng/testing/validate-typescript-apphost.ps1 index 3ad7b7387..bebaba063 100644 --- a/eng/testing/validate-typescript-apphost.ps1 +++ b/eng/testing/validate-typescript-apphost.ps1 @@ -240,4 +240,4 @@ if ($cleanupFailures.Count -gt 0) { if ($null -ne $primaryError) { throw $primaryError -} \ No newline at end of file +}