Export canonical TypeScript API data from the CLI - #19032
Export canonical TypeScript API data from the CLI#19032Adam Ratzman (adamint) wants to merge 70 commits into
Conversation
Extract every TypeScript-specific resolution decision out of AtsTypeScriptCodeGenerator into TypeScriptApiProjector: type mapping, options flattening, callback shaping, promise wrapping, and fluent return selection. The generator consumes the resolved model instead of recomputing those decisions inline, and TypeScriptApiExportWriter serializes the same model into a schema version 1 canonical export. Documentation that reconstructs signatures from raw ATS drifts from the SDK that actually ships (microsoft#17608). Sharing one projection makes that drift impossible: the generated aspire.mts snapshots are byte-identical, and a new test asserts every exported declaration appears verbatim in the corresponding generated public interface. Ownership is resolved per capability rather than per type, mirroring AtsContextFilter, so a package that extends another package's resource documents its own members without republishing the referenced type. Referenced types contribute opaque declaration fragments keyed by their real owner, which lets a manifest concatenate packages and type-check without site-authored shims. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 819baaf4-91c3-44ce-8004-3dbc9110f810
Adds IApiReferenceExporter as an optional companion to ICodeGenerator, so a language provider can describe the surface it generates without every provider being forced to. AtsTypeScriptCodeGenerator implements it by building the same TypeScriptApiProjector code generation uses, which is what keeps documentation from drifting from emitted source. RemoteHost exposes it as an authenticated exportApi RPC that resolves the existing code generator and then requires the optional interface, rather than introducing a second discovery mechanism. The provider's JSON document is returned verbatim; the payload schema belongs to the language provider. Also fixes a reference-closure bug this uncovered. Types owned by the selected assembly were seeded into AtsContextFilter's included sets directly, so the "was this newly added?" guard refused to walk their own members. An owned DTO exposing an enum from a non-Aspire dependency kept the DTO and dropped the enum, and code generation then failed on the dangling reference -- generateCode for Aspire.Hosting hit this too. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 819baaf4-91c3-44ce-8004-3dbc9110f810
Adds a hidden `aspire sdk export` that asks a scanner AppHost for the canonical API reference of one package in one language and writes it to stdout. Because documentation pipelines consume it, stdout carries exactly one JSON document and every status message goes to stderr, so `aspire sdk export ... > api.json` produces a usable file. The package version must be exact. A document published under a range would describe a different SDK after the next restore, so floating versions are rejected before restore rather than resolved. With no --package, the command defaults to Aspire.Hosting at this CLI's own identity version, which is the whole point: the docs describe the SDK this CLI generates against. SdkCommandPreparation holds only what dump and export genuinely share -- argument parsing, scanner AppHost setup, exporting-assembly discovery. Dump keeps its own serialization and is deliberately not routed through the canonical exporter; a test asserts its payload still has the capabilities shape and no schemaVersion. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 819baaf4-91c3-44ce-8004-3dbc9110f810
The export contract promises that concatenating a manifest's declaration fragments type-checks without site-authored shims. Running TypeScript over real Aspire.Hosting and Aspire.Hosting.Redis exports showed it did not. Handle types with no generated wrapper class surface in signatures under their raw handle alias name, but the fragment pass derived a class name instead, so it declared a symbol nothing referenced and left the referenced alias undefined. Emit the same alias the generator emits. The runtime fragment was also missing Handle, InputType and AbortSignal, and MarshalledHandle was missing $type. Keep sdk export's own --help reachable: Hidden on a subcommand suppresses its help output, and the parent sdk command is already hidden. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 819baaf4-91c3-44ce-8004-3dbc9110f810
The scanner AppHost does not reference the language's code generation package by default, so the server loaded no generators and every export failed with "No code generator found for language: typescript". sdk generate already adds the package for exactly this reason. Verified end to end against the repo-local AppHost server: exporting Aspire.Hosting and Aspire.Hosting.Redis now writes schema version 1 documents to stdout with nothing on stderr, and TypeScript type-checks their combined declaration fragments with no errors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 819baaf4-91c3-44ce-8004-3dbc9110f810
A package that extends a type another package owns emitted its
contribution as a normal interface item: same "interface:{name}" stable
ID as the owning package's item, and owningAssembly pointing at the
extending package. Across a manifest that collides with the real page and
misattributes the type, which the export contract explicitly forbids.
Give these items an augmentation kind, an "augmentation:{name}" ID, and
the type's real owning assembly. The declaration fragments already used
this split; the items now match.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 819baaf4-91c3-44ce-8004-3dbc9110f810
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 19032Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 19032" |
There was a problem hiding this comment.
Pull request overview
Adds canonical TypeScript API export through aspire sdk export, sharing projection logic with generated SDK output.
Changes:
- Adds the export model, serializer, RPC, and CLI command.
- Fixes ATS reference-closure handling.
- Adds schema, CLI, RPC, and regression tests.
Show a summary per file
| File | Description |
|---|---|
src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs |
Uses shared projection and implements export. |
src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptApiProjector.cs |
Centralizes TypeScript API projection. |
src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptApiModel.cs |
Defines the export model. |
src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptApiExportWriter.cs |
Serializes schema v1. |
src/Aspire.TypeSystem/IApiReferenceExporter.cs |
Adds the exporter contract. |
src/Aspire.TypeSystem/ApiReferenceExportOptions.cs |
Adds package export options. |
src/Aspire.TypeSystem/api/Aspire.TypeSystem.cs |
Updates the generated API baseline. |
src/Aspire.Hosting.RemoteHost/CodeGeneration/CodeGenerationService.cs |
Exposes the export RPC. |
src/Aspire.Hosting.RemoteHost/CodeGeneration/CodeGeneratorResolver.cs |
Resolves exporters. |
src/Aspire.Hosting.RemoteHost/Diagnostics/RemoteHostProfilingTelemetry.cs |
Adds export telemetry. |
src/Aspire.Hosting.RemoteHost/AtsContextFilter.cs |
Expands referenced type closure. |
src/Aspire.Hosting.RemoteHost/Aspire.Hosting.RemoteHost.csproj |
Grants test internals access. |
src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs |
Implements sdk export. |
src/Aspire.Cli/Commands/Sdk/SdkCommandPreparation.cs |
Shares scanner preparation. |
src/Aspire.Cli/Commands/Sdk/SdkDumpCommand.cs |
Uses shared preparation. |
src/Aspire.Cli/Commands/Sdk/SdkCommand.cs |
Registers the subcommand. |
src/Aspire.Cli/Projects/IAppHostRpcClient.cs |
Adds the client contract. |
src/Aspire.Cli/Projects/AppHostRpcClient.cs |
Implements export RPC invocation. |
src/Aspire.Cli/Program.cs |
Registers command services. |
tests/Aspire.Hosting.RemoteHost.Tests/CodeGeneration/ApiReferenceExportTests.cs |
Tests RPC behavior. |
tests/Aspire.Hosting.RemoteHost.Tests/AtsContextFilterTests.cs |
Covers closure expansion. |
tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs |
Tests export parity and declarations. |
tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/AtsTypeScriptCodeGeneratorTests.ApiExport.verified.json |
Captures schema output. |
tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/AtsTypeScriptCodeGeneratorTests.ApiDeclarations.verified.txt |
Captures declaration fragments. |
tests/Aspire.Cli.Tests/Commands/Sdk/SdkExportCommandTests.cs |
Tests CLI behavior. |
tests/Aspire.Cli.Tests/TestServices/FakeAppHostServerSession.cs |
Extends the fake RPC client. |
tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs |
Registers the command in tests. |
Review details
Suppressed comments (3)
src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs:128
- The code generator is restored at the running CLI version rather than the requested package version. Exporting an older/newer package can therefore apply a different release's projection rules (or fail when
--sourcecontains only the requested release), so the result is not canonical for the requestedName@Version. Restore the generator atpackageVersion, as normal AppHost generation does for its effective SDK version.
integrations.Add(IntegrationReference.FromPackage(codeGenPackage, ExecutionContext.IdentityVersion));
src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs:76
- This command promises that every human-readable message goes to stderr, but it never switches
InteractionService.Consolefrom its default stdout route. As a result, preparation diagnostics and the--outputsuccess message can be written to stdout. Route the command's human output to stderr before any discovery or preparation work.
var language = parseResult.GetValue(s_languageOption)!;
src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs:119
- An explicit
Aspire.Hosting@Versionis never restored here. Both scanner implementations ignore thesdkVersionargument, so a CLI running a different version scans its bundled/repositoryAspire.Hostingassembly and then labels that surface with the requested version. The command therefore cannot honor its exactName@Versioncontract for the core package; the scanner must load the requested core package version rather than skipping it.
if (!string.Equals(packageName, CorePackageName, StringComparison.OrdinalIgnoreCase))
{
integrations.Add(reference);
}
- Files reviewed: 25/27 changed files
- Comments generated: 4
- Review effort level: Balanced
The generated capability scanner writes a Directory.Packages.props that turns central package management on, which rejects an inline Version attribute with NU1008. Any integration outside the repo failed to build, so sdk export could not be pointed at a third-party package. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 819baaf4-91c3-44ce-8004-3dbc9110f810
Ownership only transfers to PreparedSdkSession once one is returned. A throw from StartAsync or GetRpcClientAsync left the spawned scanner running and holding the temp directory. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 819baaf4-91c3-44ce-8004-3dbc9110f810
Fragments built from raw string literals carry whatever line endings the source was checked out with, so a Windows build disagreed with a Linux build about identical declarations. Consumers deduplicate a manifest by comparing content for the same ID, so the text has to be stable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 819baaf4-91c3-44ce-8004-3dbc9110f810
Every integration that extends DistributedApplicationBuilder produced the same augmentation item ID, which recreated the cross-package collision the augmentation kind exists to avoid, so the contributing package is now part of the ID. CreateBuilderOptions also gained its client-only throwOnPendingRejections property from the module emitter alone, so the export described a smaller interface than the one we ship. That list moved to the projector and both paths read it from there. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 819baaf4-91c3-44ce-8004-3dbc9110f810
There was a problem hiding this comment.
Review details
Suppressed comments (3)
src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptApiProjector.cs:276
- This hardcodes the generated options-bag parameter name to
options, while the source emitter callsGetPublicOptionsParameterNameto avoid collisions. A capability that already has a parameter namedoptionsis therefore exported with a different—or even duplicate and invalid—signature from the generated.mts, defeating the canonical-source contract. Resolve and pass the collision-safe name here too.
var parameterList = BuildPublicParameterList(
requiredParams,
hasOptionals,
optionsTypeName,
trailingCancellationToken: GetTrailingCancellationTokenParameter(optionalParams));
src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptApiProjector.cs:744
- The structured
parametersarray is still built from raw ATS parameters instead of the resolved public signature. For example, the checked-in export declareswithOptionalString(options?: WithOptionalStringOptions)but reportsvalueandenabledas its parameters. Any consumer rendering the structured fields will reconstruct the same stale positional API this PR is intended to eliminate. Build this list from the required parameters plus the resolved options-bag and trailing cancellation-token parameters.
var parameters = capability.Parameters
.Where(p => builderModel is null || p.Name != targetParamName)
.Select(p => new TypeScriptApiParameter
{
Name = p.Name,
DeclaredType = MapParameterToTypeScript(p),
IsOptional = p.IsOptional || p.IsNullable,
Summary = p.Documentation?.Summary
})
src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs:194
- The new CLI command has only fake project/session/RPC tests; there is no CLI end-to-end test that starts the real scanner and validates the machine-output contract. That leaves package/codegen restoration, RPC registration, and stdout/stderr separation untested—the exact integration points this command introduces. Add a Hex1b CLI E2E test using a local-hive package that invokes
sdk export, redirects stdout, parses the JSON, and verifies stderr remains separate.
await using var session = await SdkCommandPreparation.PrepareSessionAsync(
_appHostServerProjectFactory,
_serverSessionFactory,
InteractionService,
_logger,
"aspire-sdk-export-",
sdkVersion,
integrations,
packageSource,
cancellationToken);
- Files reviewed: 27/29 changed files
- Comments generated: 1
- Review effort level: Balanced
The scanner never honored a requested core version. Both PrepareAsync implementations accept sdkVersion and ignore it, so `sdk export --package Aspire.Hosting@13.0.0` returned byte-identical JSON to a 13.5.0-dev export, relabelled 13.0.0. That is the stale-signature problem this command exists to fix. Honoring the request is not possible: the prebuilt server bundles core and the RPC host compiled together, so loading a foreign core would break the RPC contract. Reject the skew instead, with a message pointing at the CLI that can produce the requested export. Integration packages are unaffected: they become real PackageReferences and already resolve to the requested version. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 819baaf4-91c3-44ce-8004-3dbc9110f810
There was a problem hiding this comment.
Review details
Suppressed comments (3)
src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs:135
- A non-core Aspire package can request any version here, but the scanner and TypeScript code-generation package are still pinned to the running CLI's
IdentityVersion. Thus a 13.6 CLI exportingAspire.Hosting.Redis@13.5.0applies the 13.6 signature-shaping generator and labels the result as 13.5.0, recreating the version drift this command is intended to prevent. Either restore/run the matching SDK and generator or reject officialAspire.Hosting.*versions that differ fromIdentitySdkVersion, as is already done for core.
else
{
integrations.Add(reference);
}
src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptApiProjector.cs:276
ResolveMethodSignaturealways names the synthesized optional-parameter bagoptions, but the source emitter callsGetPublicOptionsParameterNameto avoid collisions. Real generated APIs such asaddCSharpAppandpromptProgresstherefore useoptionsBag, while this export reportsoptions, reintroducing the docs/generated-signature drift this PR is intended to remove. Compute and pass the same collision-safe name here.
var parameterList = BuildPublicParameterList(
requiredParams,
hasOptionals,
optionsTypeName,
trailingCancellationToken: GetTrailingCancellationTokenParameter(optionalParams));
src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptApiProjector.cs:740
- The structured
parametersarray is built from the original ATS parameters rather than the public signature. For example, the snapshot declaresaddTestRedis(name, options?: AddTestRedisOptions)but serializesnameandport, with nooptionsparameter. Consumers using these schema fields will reconstruct the same stale positional API this export is meant to eliminate. Build this list from the required public parameters plus the synthesized options bag (and any separately emitted cancellation token).
var parameters = capability.Parameters
.Where(p => builderModel is null || p.Name != targetParamName)
.Select(p => new TypeScriptApiParameter
{
Name = p.Name,
- Files reviewed: 27/29 changed files
- Comments generated: 1
- Review effort level: Balanced
The command always emits JSON, so it has no --format option for BaseCommand's json redirect to key off. Preparation diagnostics and the --output success message therefore went to stdout and corrupted the document when a caller piped it. DisplaySuccess takes no per-call override, so it could not opt out. Route the interaction service to stderr at command entry. The JSON write already overrides back to stdout explicitly, and an explicit override wins over the service setting. SdkExportSendsProgressToStderrOnly asserted on the per-call override, which is null for these calls, so it passed vacuously. It now resolves the effective destination and covers the --output path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 819baaf4-91c3-44ce-8004-3dbc9110f810
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
There was a problem hiding this comment.
Review details
Suppressed comments (5)
tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/PriorContractBindingTests.cs:1
- This test records only the simple type name (
TypeReference.Name) and member name. IfAspire.TypeSystemever contains two types with the same simple name in different namespaces, the guard can produce false negatives/positives. Consider including the namespace (or full name) inTypeSystemMemberReferenceand matching against a baseline representation that’s also unambiguous (e.g., parsingnamespaceblocks + type names, or using aGenAPI-like full identifier format).
tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/WithDataVolumeOptionsMerged.verified.ts:1 - This line appears to include a leading UTF-8 BOM character (U+FEFF) before
export. If this is accidental (e.g., introduced by an editor/encoding change), it can create hard-to-spot diffs and inconsistent snapshot behavior across environments. Consider normalizing snapshot encodings (no BOM) and ensuring generated output snapshots don’t depend on BOM presence.
src/Aspire.Hosting.RemoteHost/CodeGeneration/CodeGeneratorResolver.cs:169 GetLoadableTypes(...)logs a Warning onReflectionTypeLoadException. It’s called from both generator discovery and exporter discovery, so a single type-load failure will be logged twice (once per pass), contradicting the comment that the failure “was already reported by DiscoverGenerators.” Consider caching the loadable type list per assembly (or adding alogFailures/alreadyLoggedflag) so exporter discovery can reuse the prior result without re-logging.
{
var assemblyName = assembly.GetName().Name;
// An assembly with no exporter is the normal case (most languages generate code they
// cannot yet describe), so unlike generator discovery this pass never warns about
// finding nothing. A type-load failure was already reported by DiscoverGenerators.
foreach (var type in GetLoadableTypes(assembly, assemblyName, out _))
{
if (type.IsAbstract || type.IsInterface || !typeof(IApiReferenceExporter).IsAssignableFrom(type))
{
continue;
}
src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs:586
GetLocalProjectSubstitutionenumerates allsrc/*directories on every call. InCreateProjectFile, this is invoked per integration, making substitution checks O(integrations × srcDirs). Consider caching a case-insensitive map of{ packageName -> projectPath }(or caching just thesrcdirectory list) for the lifetime of theDotNetBasedAppHostServerProjectinstance to avoid repeated enumeration.
public LocalProjectSubstitution? GetLocalProjectSubstitution(string packageName)
{
if (!packageName.StartsWith("Aspire.Hosting", StringComparison.OrdinalIgnoreCase))
{
return null;
}
var srcPath = Path.Combine(_repoRoot, "src");
if (!Directory.Exists(srcPath))
{
return null;
}
// The directory listing settles the spelling on every platform. Probing
// Path.Combine(src, packageName) instead would hand back the caller's spelling wherever
// File.Exists is case-insensitive, so the same request produced two different project paths
// depending on the filesystem. Enumerate and compare rather than passing the caller's name
// as a search pattern, so a package id that happens to contain a wildcard cannot match a
// directory it does not name.
//
// Enumerating can throw where the old File.Exists probe could not, and this runs on the
// `aspire run` path via CreateProjectFile, so an unreadable or concurrently removed src/
// reports "no substitution" the same way GetRepositoryVersionPrefix does rather than
// costing the caller its scanner.
try
{
foreach (var directory in Directory.EnumerateDirectories(srcPath))
{
var canonicalName = Path.GetFileName(directory);
if (!string.Equals(canonicalName, packageName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
var projectPath = Path.Combine(directory, $"{canonicalName}.csproj");
return File.Exists(projectPath)
? new LocalProjectSubstitution(projectPath, GetRepositoryVersionPrefix())
: null;
}
src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptApiExportWriter.cs:18
- The remarks say the writer “drops empty collections,” but the implementation always emits the top-level
modulesanddeclarationsarrays (even if empty). If the schema requires these properties, the comment should be narrowed (e.g., “drops empty optional collections likemembers,examples, …”) to avoid misleading contract readers.
/// The document is written by hand rather than through reflection-based serialization because the
/// shape is a published contract that documentation sites bind to. Writing it explicitly makes the
/// contract reviewable in one place, keeps property order stable so exports diff cleanly between SDK
/// versions, and drops empty collections and null strings so a version bump only shows real API
/// changes.
/// </remarks>
- Files reviewed: 65/68 changed files
- Comments generated: 0 new
- Review effort level: Lite
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c235246b-d021-4d8e-b041-cc4984674ebe
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c235246b-d021-4d8e-b041-cc4984674ebe
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c235246b-d021-4d8e-b041-cc4984674ebe
There was a problem hiding this comment.
Review details
Suppressed comments (4)
src/Aspire.Hosting.RemoteHost/CodeGeneration/CodeGenerationService.cs:385
- This package-to-assembly lookup is unavailable in repository/dev mode:
DotNetBasedAppHostServerProjectneither creates a probe manifest nor setsASPIRE_INTEGRATION_PROBE_MANIFEST_PATH. Consequently this branch is always skipped there, and packages whose ID differs from their assembly name—or that own multiple assemblies—still fail or export only the ID-matching assembly. Generate equivalent ownership metadata for the dotnet-based scanner, not only the prebuilt scanner.
if (_assemblyLoader.TryGetPackageAssemblyNamesFromProbePaths(
packageName,
packageVersion,
out var manifestAssemblyNames))
src/Aspire.Hosting.RemoteHost/CodeGeneration/CodeGenerationService.cs:404
- The probe-path branch preserves the caller's casing as the exported package identity. Thus
--package contoso.aspire.widgets@...successfully maps case-insensitively but emitscontoso.aspire.widgets, despite this code's stated requirement that consumers receive the canonical package ID. Carry the restored package ID through the probe metadata and assign that canonical value here.
canonicalPackageName = packageName;
return exportingAssemblyNames;
src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs:107
- C# is returned by language discovery with an empty code-generation package name. This condition accepts the empty string and
IntegrationReference.FromPackagethen throws before the RPC can report that C# has no API exporter. Treat an empty package name as absent so unsupported C# requests reach the structuredInvalidParamspath.
if (codeGenerationPackage is not null &&
!integrations.Any(integration =>
integration.Name.Equals(codeGenerationPackage, StringComparison.OrdinalIgnoreCase)))
src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs:273
SemVersionis not the NuGet version grammar, so this rejects valid exact NuGet versions such as1.2.3.4. NuGet explicitly supports a fourthRevisionsegment and recommendsNuGetVersion.Parsefor package versions. Parse and normalize withNuGet.Versioning.NuGetVersionso every exact version accepted by restore is accepted by this command.
if (requestedVersion.Any(char.IsWhiteSpace) ||
!SemVersion.TryParse(requestedVersion, SemVersionStyles.Any, out var parsedVersion))
{
errorMessage = $"Invalid version '{requestedVersion}'. Expected an exact NuGet version.";
- Files reviewed: 25/27 changed files
- Comments generated: 1
- Review effort level: Balanced
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c235246b-d021-4d8e-b041-cc4984674ebe
There was a problem hiding this comment.
Review details
Suppressed comments (3)
src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs:187
- Routing an exact range through this branch still adds it to
otherPackages, which is emitted asPackageReference Version="[x]". This generated project enables central package management, so repository-mode restores reject the reference with NU1008 before the export RPC can run. UseVersionOverridefor these explicit versions and make the regression test perform a restore rather than only inspecting the XML.
// An exact range is an explicit request to restore that package, used by sdk export so
// the document cannot be labelled with a package version while describing checkout code.
else if (integration.Name.StartsWith("Aspire.Hosting", StringComparison.OrdinalIgnoreCase) &&
!IsExactVersionRange(integration.Version))
src/Aspire.Hosting.RemoteHost/CodeGeneration/CodeGenerationService.cs:345
- Filtering to one package before constructing the projector makes options-interface collision resolution package-local. For example, Event Hubs and Service Bus can each export an incompatible
runAsEmulatorbag asRunAsEmulatorOptions, while full TypeScript generation sees both and suffixes one; concatenating these exports then conflicts and the documented signatures no longer match generated bindings. Options names must be deterministic from assembly/capability identity, or be resolved against the full manifest before documentation is scoped.
var context = AtsContextFilter.FilterForApiExport(
fullContext,
exportingAssemblyNames);
src/Aspire.Hosting.RemoteHost/CodeGeneration/CodeGenerationService.cs:403
- This manifest-backed path preserves the caller's casing instead of a canonical package ID. NuGet package IDs are case-insensitive and the global-packages path used for ownership is lowercased, so
--package aspire.hosting.redis@...succeeds but emitspackage.name = "aspire.hosting.redis"; consumers keyed by the publishedAspire.Hosting.Redisidentity will not find it. Carry the canonical package ID from restore metadata into the probe manifest and use it here.
canonicalPackageName = packageName;
- Files reviewed: 25/27 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c235246b-d021-4d8e-b041-cc4984674ebe
There was a problem hiding this comment.
Review details
Suppressed comments (2)
src/Aspire.Hosting.RemoteHost/CodeGeneration/CodeGenerationService.cs:403
- This branch preserves the caller's package-ID casing even though the fallback immediately below canonicalizes it to avoid publishing an identity consumers cannot find. For example,
--package contoso.aspire.widgets@1.2.3maps successfully because all comparisons are case-insensitive, but the JSON still recordscontoso.aspire.widgets. Carry the canonical package ID from restore/probe metadata and use it here; the assembly name is not a safe substitute because package and assembly names may differ.
canonicalPackageName = packageName;
src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs:274
- This validates a NuGet package version with a SemVer parser, but NuGet's version grammar is broader: for example,
1.2.3.4is a valid exact NuGet version (Revisionis the fourth component) and is rejected here before restore. Parse and normalize withNuGet.Versioning.NuGetVersionso every exact version accepted by NuGet can be exported.
if (requestedVersion.Any(char.IsWhiteSpace) ||
!SemVersion.TryParse(requestedVersion, SemVersionStyles.Any, out var parsedVersion))
{
errorMessage = $"Invalid version '{requestedVersion}'. Expected an exact NuGet version.";
return false;
- Files reviewed: 25/27 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3253974d-4f18-486f-863c-281a607656d4
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3253974d-4f18-486f-863c-281a607656d4
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3253974d-4f18-486f-863c-281a607656d4
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3253974d-4f18-486f-863c-281a607656d4
There was a problem hiding this comment.
Review details
Suppressed comments (2)
src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs:187
- Using an exact NuGet range as the signal for export mode changes normal repository behavior too.
aspire.config.jsonand legacy settings pass arbitrary explicit version strings throughIntegrationReference.FromPackage(AspireConfigFile.cs:356-383,AspireJsonConfiguration.cs:196-223), so a normal package entry such as"Aspire.Hosting.Redis": "[13.1.0]"now bypasses the checkout project and restores NuGet instead. This contradicts the PR's stated export-only behavior. Carry an explicit “disable local project substitution” flag fromSdkExportCommandrather than inferring the command mode from version syntax.
// An exact range is an explicit request to restore that package, used by sdk export so
// the document cannot be labelled with a package version while describing checkout code.
else if (integration.Name.StartsWith("Aspire.Hosting", StringComparison.OrdinalIgnoreCase) &&
!IsExactVersionRange(integration.Version))
src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs:122
- The new command's only CLI tests replace both
IAppHostServerProjectandIAppHostRpcClient, so they never exercise the behavior introduced here: exact NuGet restore, launching the real RemoteHost, discovering the exporter, and preserving the stdout/stderr contract across the subprocess boundary. There is also nosdk exportcoverage inAspire.Cli.EndToEnd.Tests. Add a CLI end-to-end test using a local/offline package source that invokes this command and parses stdout as JSON; otherwise the primary user path can fail while all current tests pass.
var exitCode = await ExportApiAsync(
languageInfo?.CodeGenerator ?? language,
packageName,
packageVersion,
integrations,
cancellationToken);
- Files reviewed: 31/33 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d65b598f-72dd-4fca-bdc4-8debbbd7f821
Preserve normal repository substitution for exact version ranges and cover the real export subprocess against the installed package hive. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d65b598f-72dd-4fca-bdc4-8debbbd7f821
There was a problem hiding this comment.
Review details
Suppressed comments (1)
src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptApiReferenceExporter.cs:51
- A pre-canceled request still constructs
TypeScriptApiProjector, whose constructor resolves the entire ATS context, beforeBuildApiModelobserves the token. Large exports therefore do substantial work after cancellation and can delay RPC shutdown. Check the token before starting projection; cancellation duringResolvemay also need to be threaded through separately.
// Build the projector from the same context the generator would use, so the exported
// documentation describes the exact signatures generation would emit rather than a
// second, independently derived reading of the ATS context.
var projector = new TypeScriptApiProjector(context);
- Files reviewed: 33/35 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Check cancellation before resolving the TypeScript projection and cover the ordering explicitly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d65b598f-72dd-4fca-bdc4-8debbbd7f821
There was a problem hiding this comment.
Review details
Suppressed comments (1)
src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs:72
IdentitySdkVersioncan be supplied byASPIRE_CLI_VERSION, but both scanner project implementations ignore thesdkVersionpassed toPrepareAsyncand load the physical CLI/checkout core assemblies. An override therefore makes the default command export the current surface while labeling it as another SDK version—the stale-signature failure this command is meant to prevent. Validate against the physical bundled SDK version (while allowing an equivalent install-sidecar identity) before exporting core.
var packageName = CorePackageName;
var packageVersion = ExecutionContext.IdentitySdkVersion;
- Files reviewed: 33/35 changed files
- Comments generated: 2
- Review effort level: Balanced
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d65b598f-72dd-4fca-bdc4-8debbbd7f821
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d65b598f-72dd-4fca-bdc4-8debbbd7f821
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d65b598f-72dd-4fca-bdc4-8debbbd7f821
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
Description
Adds
aspire sdk export, which emits a deterministic TypeScript API reference as JSON. The command exportsAspire.Hostingat the running CLI SDK version by default, or an exactPackageName@Versionsupplied with--package.This is the producer side of fixing #17608: generated TypeScript signatures and the exported documentation model now share the same projector, so optional-parameter shaping, promise wrappers, entry points, DTOs, enums, and declaration fragments cannot drift independently.
The focused implementation includes:
IApiReferenceExporter.lib,ref, andruntimes/<rid>/libassets.The reduction intentionally excludes source selection, output-file handling, checkout provenance/substitution machinery, compatibility shims, and normal-build manifest expansion.
Contributes to #17608.
Validation
/etc/resolv.confis denied.Checklist
<remarks />and<code />elements on your triple slash comments?