Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ nuget
target

node_modules
**/.modules/
**/*.AppHost.TypeScript/nuget.config
218 changes: 218 additions & 0 deletions eng/testing/validate-typescript-apphost.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
#!/usr/bin/env pwsh

param(
[Parameter(Mandatory = $true)]
[string]$AppHostPath,

[Parameter(Mandatory = $true)]
[string]$PackageProjectPath,

[Parameter(Mandatory = $true)]
[string]$PackageName,

[Parameter(Mandatory = $true)]
[string[]]$WaitForResources,

[string[]]$RequiredCommands = @(),

[string]$PackageVersion = "",

[ValidateSet("healthy", "up", "down")]
[string]$WaitStatus = "healthy",

[int]$WaitTimeoutSeconds = 180
)

$ErrorActionPreference = "Stop"

function Invoke-ExternalCommand {
param(
[Parameter(Mandatory = $true)]
[string]$FilePath,

[Parameter(Mandatory = $true)]
[string[]]$Arguments
)

& $FilePath @Arguments
if ($LASTEXITCODE -ne 0) {
$joinedArguments = [string]::Join(" ", $Arguments)
throw "Command failed with exit code ${LASTEXITCODE}: $FilePath $joinedArguments"
}
}

function Invoke-CleanupStep {
param(
[Parameter(Mandatory = $true)]
[string]$Description,

[Parameter(Mandatory = $true)]
[scriptblock]$Action,

[System.Collections.Generic.List[string]]$Failures = $null
)

try {
& $Action
}
catch {
$message = "Cleanup step '$Description' failed: $($_.Exception.Message)"
if ($null -ne $Failures) {
$Failures.Add($message)
return
}

throw $message
}
}

$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"
}

if ($WaitForResources.Count -eq 1) {
$splitOptions = [System.StringSplitOptions]::RemoveEmptyEntries -bor [System.StringSplitOptions]::TrimEntries
$WaitForResources = $WaitForResources[0].Split(",", $splitOptions)
}

if ($RequiredCommands.Count -eq 1) {
$splitOptions = [System.StringSplitOptions]::RemoveEmptyEntries -bor [System.StringSplitOptions]::TrimEntries
$RequiredCommands = $RequiredCommands[0].Split(",", $splitOptions)
}

foreach ($commandName in $RequiredCommands) {
if ($null -eq (Get-Command $commandName -ErrorAction SilentlyContinue)) {
throw "Required command '$commandName' was not found on PATH."
}
}

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
)

$config = $originalConfig | ConvertFrom-Json -AsHashtable
if ($null -eq $config["packages"]) {
$config["packages"] = [ordered]@{}
}

$config["packages"][$PackageName] = $PackageVersion
$config | ConvertTo-Json -Depth 10 | Set-Content -Path $configPath -NoNewline

@"
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="local-polyglot" value="$localSource" />
</packageSources>
</configuration>
"@ | Set-Content -Path $nugetConfigPath -NoNewline

Push-Location $appHostDirectory
try {
Invoke-ExternalCommand "npm" @("ci")
Invoke-ExternalCommand "aspire" @(
"restore",
"--apphost", $resolvedAppHostPath,
"--non-interactive"
)
Invoke-ExternalCommand "npx" @("tsc", "--noEmit")
}
finally {
Pop-Location
}

Invoke-ExternalCommand "aspire" @(
"start",
"--apphost", $resolvedAppHostPath,
"--isolated",
"--format", "Json",
"--non-interactive"
)
$appStarted = $true

foreach ($resource in $WaitForResources) {
Invoke-ExternalCommand "aspire" @(
"wait",
$resource,
"--status", $WaitStatus,
"--apphost", $resolvedAppHostPath,
"--timeout", $WaitTimeoutSeconds
)
}

Invoke-ExternalCommand "aspire" @(
"describe",
"--apphost", $resolvedAppHostPath,
"--format", "Json"
)
}
catch {
$primaryError = $_
}
finally {
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 {
if (Test-Path $nugetConfigPath) {
Remove-Item $nugetConfigPath -Force
}
} -Failures $cleanupFailures

Invoke-CleanupStep -Description "remove local package source" -Action {
if (Test-Path $localSource) {
Remove-Item $localSource -Recurse -Force
}
} -Failures $cleanupFailures

Invoke-CleanupStep -Description "stop Aspire app" -Action {
if ($appStarted) {
Invoke-ExternalCommand "aspire" @(
"stop",
"--apphost", $resolvedAppHostPath
)
}
} -Failures $cleanupFailures
}

if ($cleanupFailures.Count -gt 0) {
$cleanupFailureMessage = "Cleanup failures:${Environment.NewLine}$($cleanupFailures -join [Environment]::NewLine)"
if ($null -ne $primaryError) {
Write-Error -Message $cleanupFailureMessage -ErrorAction Continue
}
else {
throw $cleanupFailureMessage
}
}

if ($null -ne $primaryError) {
throw $primaryError
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { mkdirSync, mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createBuilder } from './.modules/aspire.js';

const builder = await createBuilder();
const bindMountRoot = mkdtempSync(join(tmpdir(), 'activemq-polyglot-'));
const artemisDataPath = join(bindMountRoot, 'artemis-data');
const artemisConfPath = join(bindMountRoot, 'artemis-conf');

mkdirSync(artemisDataPath, { recursive: true });
mkdirSync(artemisConfPath, { recursive: true });

const mqPassword = await builder.addParameterWithValue("mq-password", "admin", {
secret: true
});
const mqUser = await builder.addParameterWithValue("mq-user", "admin", {
publishValueAsDefault: true
});

// addActiveMQ — ActiveMQ Classic with all parameters
const classic = await builder.addActiveMQ("classic", {
userName: mqUser,
password: mqPassword,
port: 36161,
scheme: "activemq",
webPort: 38161
});

// addActiveMQ — minimal overloads with explicit credentials for repeatable startup
const classic2 = await builder.addActiveMQ("classic2", {
userName: mqUser,
password: mqPassword
});

// addActiveMQArtemis — Artemis with all parameters
const artemis = await builder.addActiveMQArtemis("artemis", {
userName: mqUser,
password: mqPassword,
port: 36162,
scheme: "tcp",
webPort: 38162
});

// addActiveMQArtemis — minimal overloads with explicit credentials for repeatable startup
const artemis2 = await builder.addActiveMQArtemis("artemis2", {
userName: mqUser,
password: mqPassword
});

// withDataVolume — fluent chaining on Classic
await classic.withDataVolume({ name: "classic-data" });

// withConfVolume — fluent chaining on Classic
await classic.withConfVolume({ name: "classic-conf" });

// withDataBindMount — bind mount on Artemis
await artemis.withDataBindMount(artemisDataPath);

// withConfBindMount — bind mount on Artemis
await artemis.withConfBindMount(artemisConfPath);

// withDataVolume + withConfVolume — chaining on Artemis
await artemis2.withDataVolume();
await artemis2.withConfVolume();

// ---- Property access on ActiveMQServerResourceBase (ExposeProperties = true) ----
const classicResource = await classic;
const _classicEndpoint = await classicResource.primaryEndpoint.get();
const _classicHost = await classicResource.host.get();
const _classicPort = await classicResource.port.get();
const _classicUri = await classicResource.uriExpression.get();
const _classicCstr = await classicResource.connectionStringExpression.get();

const classic2Resource = await classic2;
const _classic2Host = await classic2Resource.host.get();
const _classic2Port = await classic2Resource.port.get();

const artemisResource = await artemis;
const _artemisEndpoint = await artemisResource.primaryEndpoint.get();
const _artemisHost = await artemisResource.host.get();
const _artemisPort = await artemisResource.port.get();
const _artemisUri = await artemisResource.uriExpression.get();
const _artemisCstr = await artemisResource.connectionStringExpression.get();

const artemis2Resource = await artemis2;
const _artemis2Host = await artemis2Resource.host.get();
const _artemis2Port = await artemis2Resource.port.get();

await builder.build().run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"appHost": {
"path": "apphost.ts",
"language": "typescript/nodejs"
},
"profiles": {
"https": {
"applicationUrl": "https://localhost:29750;http://localhost:28931",
"environmentVariables": {
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:10975",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:13319"
}
}
},
"packages": {
"CommunityToolkit.Aspire.Hosting.ActiveMQ": ""
}
}
Loading
Loading