diff --git a/.agents/skills/container-images/SKILL.md b/.agents/skills/container-images/SKILL.md index 2aa3bea0a..ceecfc2c2 100644 --- a/.agents/skills/container-images/SKILL.md +++ b/.agents/skills/container-images/SKILL.md @@ -11,7 +11,7 @@ This skill extracts **every** container image reference from the [`microsoft/asp src/frontend/src/data/container-images.json ``` -This JSON is consumed by Astro components on the aspire.dev site to render a browsable catalog of all container images used by .NET Aspire hosting integrations. +This JSON is consumed by Astro components on the aspire.dev site to render a browsable catalog of all container images used by Aspire hosting integrations. --- diff --git a/.github/agents/community-toolkit-integration-doc-writer.agent.md b/.github/agents/community-toolkit-integration-doc-writer.agent.md index ab1a52b26..5f9b54f70 100644 --- a/.github/agents/community-toolkit-integration-doc-writer.agent.md +++ b/.github/agents/community-toolkit-integration-doc-writer.agent.md @@ -162,7 +162,7 @@ This step ensures that the documentation you've created is properly indexed and ### AppHost language parity - Follow the `doc-writer` skill's AppHost language parity guidance for all AppHost and hosting-integration examples. -- Always show both C# AppHost (`AppHost.cs`) and TypeScript AppHost (`apphost.ts`) variants inside synced `Tabs` (with `syncKey='aspire-lang'`) unless the feature is genuinely language-specific or TypeScript AppHost support does not exist yet. +- Always show both C# AppHost (`AppHost.cs`) and TypeScript AppHost (`apphost.mts`) variants inside synced `Tabs` (with `syncKey='aspire-lang'`) unless the feature is genuinely language-specific or TypeScript AppHost support does not exist yet. - Before writing a TypeScript AppHost example, verify the API exists in the TypeScript AppHost SDK. Do not invent TypeScript samples. - If TypeScript AppHost support is not available, show only the C# example without language tabs and add a note that TypeScript AppHost support for the integration is not yet available. - Use language-neutral prose around AppHost examples, such as "Add a resource to your AppHost" instead of C#-specific method instructions. diff --git a/.github/astro.instructions.md b/.github/astro.instructions.md index dcb471173..867d8936f 100644 --- a/.github/astro.instructions.md +++ b/.github/astro.instructions.md @@ -230,7 +230,7 @@ Bad: ```md - **AppHost Project:** the orchestrator project -- **integration**: A NuGet package that configures a service or client for use with .NET Aspire +- **integration**: A NuGet package that configures a service or client for use with Aspire ``` ### Parentheses diff --git a/.github/skills/aspire/SKILL.md b/.github/skills/aspire/SKILL.md index bbdc8be52..2efe9d826 100644 --- a/.github/skills/aspire/SKILL.md +++ b/.github/skills/aspire/SKILL.md @@ -5,7 +5,7 @@ description: "Orchestrates Aspire distributed applications using the Aspire CLI # Aspire Skill -This repository uses Aspire to orchestrate its distributed application. Resources are defined in the AppHost project (`apphost.cs` or `apphost.ts`). +This repository uses Aspire to orchestrate its distributed application. Resources are defined in the AppHost project (`apphost.cs` or `apphost.mts`). ## CLI command reference diff --git a/.github/skills/doc-writer/SKILL.md b/.github/skills/doc-writer/SKILL.md index 0b782d768..3e3719734 100644 --- a/.github/skills/doc-writer/SKILL.md +++ b/.github/skills/doc-writer/SKILL.md @@ -73,8 +73,36 @@ description: A brief summary of the page content (required for SEO) Optional frontmatter fields: - `next: false` - Disable "Next page" link for terminal pages +- `seoTitle` - Override the page's `og:title` / `twitter:title` only, + without touching the visible H1 or sidebar label. Use this **only** + when the natural H1 must stay short (commands, terse labels). When + set, the value is emitted verbatim — no `· Aspire` suffix is appended. - Custom metadata as needed by Starlight theme +#### SEO length targets + +The site uses Open Graph metadata to render social cards and feed SEO +tooling. To keep previews scannable on every social network and to +avoid the "title too short / description too long" lints that surface +on Yoast, LinkedIn, and the search-console reports, follow these +length targets when authoring frontmatter: + +| Field | Composed length target | Hard limit | +| ------------- | ---------------------: | ---------: | +| `title` | 41-51 characters | 70 characters | +| `seoTitle` | 50-60 characters | 70 characters | +| `description` | 110-160 characters | 200 characters (auto-truncated) | + +`title` becomes `og:title` composed as `${title} · Aspire`, so the +target window leaves room for the 9-character suffix. `seoTitle` +overrides the composition outright — write the full string yourself. + +Surface keywords from the article body itself in the description +(verbs, integration names, API surfaces). The CI guard at +`tests/unit/seo-lengths.vitest.test.ts` fails when any English page +strays outside the wider 30-65 / 80-200 character guard ranges, so a +draft can land slightly off-target and tighten in follow-ups. + ### Required Imports Import Starlight components at the top of your MDX file: @@ -368,8 +396,8 @@ builder.Build().Run(); ```` ````mdx -```typescript title="apphost.ts" -import { createBuilder } from "./.modules/aspire.js"; +```typescript title="apphost.mts" +import { createBuilder } from "./.aspire/modules/aspire.mjs"; const builder = await createBuilder(); @@ -407,7 +435,7 @@ For client/library packages: ## AppHost Language Parity (C# and TypeScript) -Aspire supports both **C# AppHosts** (`AppHost.cs`) and **TypeScript AppHosts** (`apphost.ts`). Documentation must treat both languages as first-class citizens. **Always show both C# and TypeScript code samples for AppHost code unless the feature is genuinely language-specific or TypeScript support does not exist yet.** Never write AppHost or hosting-integration documentation with a C#-only bias. +Aspire supports both **C# AppHosts** (`AppHost.cs`) and **TypeScript AppHosts** (`apphost.mts`). Documentation must treat both languages as first-class citizens. **Always show both C# and TypeScript code samples for AppHost code unless the feature is genuinely language-specific or TypeScript support does not exist yet.** Never write AppHost or hosting-integration documentation with a C#-only bias. ### Core Principles @@ -440,8 +468,8 @@ builder.Build().Run(); -```typescript title="apphost.ts" -import { createBuilder } from "./.modules/aspire.js"; +```typescript title="apphost.mts" +import { createBuilder } from "./.aspire/modules/aspire.mjs"; const builder = await createBuilder(); @@ -467,10 +495,10 @@ If a section heading should appear in the **On this page** table of contents, ke | Aspect | C# | TypeScript | | ---------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -| File title | `title="AppHost.cs"` | `title="apphost.ts"` | +| File title | `title="AppHost.cs"` | `title="apphost.mts"` | | Tab wrapper | Shared `` container | Shared `` container | | Tab item | `` | `` | -| Builder creation | `DistributedApplication.CreateBuilder(args)` | `import { createBuilder } from './.modules/aspire.js';` then newline for space followed by `await createBuilder();` | +| Builder creation | `DistributedApplication.CreateBuilder(args)` | `import { createBuilder } from './.aspire/modules/aspire.mjs';` then newline for space followed by `await createBuilder();` | | Method casing | PascalCase (`AddRedis`) | camelCase (`addRedis`) | | Async pattern | Synchronous fluent calls | `await` each builder call | | Build & run | `builder.Build().Run()` | `await builder.build().run()` | @@ -565,8 +593,8 @@ builder.Build().Run(); -```typescript title="apphost.ts" -import { createBuilder } from "./.modules/aspire.js"; +```typescript title="apphost.mts" +import { createBuilder } from "./.aspire/modules/aspire.mjs"; const builder = await createBuilder(); @@ -616,8 +644,8 @@ builder.Build().Run(); -```typescript title="apphost.ts" -import { createBuilder } from "./.modules/aspire.js"; +```typescript title="apphost.mts" +import { createBuilder } from "./.aspire/modules/aspire.mjs"; const builder = await createBuilder(); diff --git a/src/frontend/astro.config.mjs b/src/frontend/astro.config.mjs index cccd546a6..16be03008 100644 --- a/src/frontend/astro.config.mjs +++ b/src/frontend/astro.config.mjs @@ -117,15 +117,17 @@ export default defineConfig({ starlightSidebarTopics(sidebarTopics, { exclude: [ '**/includes/**/*', - '/support', - '/reference/api', + '/support', + '/diagnostics/aspireats001', + '/**/diagnostics/aspireats001', + '/reference/api', '/reference/api/**' ], }), starlightLinksValidator({ errorOnRelativeLinks: false, errorOnFallbackPages: false, - exclude: ['/i18n/'], + exclude: ['/i18n/', '/reference/api', '/reference/api/**'], }), starlightScrollToTop({ // https://frostybee.github.io/starlight-scroll-to-top/svg-paths/ diff --git a/src/frontend/config/redirects.mjs b/src/frontend/config/redirects.mjs index 43726d99c..cdde32fb2 100644 --- a/src/frontend/config/redirects.mjs +++ b/src/frontend/config/redirects.mjs @@ -58,6 +58,8 @@ export const redirects = { '/install-aspire-cli/': '/get-started/install-cli/', '/get-started/welcome/': '/docs/', '/get-started/installation/': '/get-started/install-cli/', + '/app-host/persistent-containers/': '/app-host/resource-lifetimes/', + '/ja/app-host/persistent-containers/': '/ja/app-host/resource-lifetimes/', // Integration -client → -connect rename '/integrations/ai/github-models/github-models-client/': '/integrations/ai/github-models/github-models-connect/', '/integrations/ai/ollama/ollama-client/': '/integrations/ai/ollama/ollama-connect/', @@ -175,6 +177,9 @@ export const redirects = { '/integrations/devtools/flagd/': '/integrations/devtools/flagd/flagd-get-started/', '/integrations/devtools/goff/': '/integrations/devtools/goff/goff-get-started/', '/integrations/devtools/mailpit/': '/integrations/devtools/mailpit/mailpit-get-started/', + '/integrations/frameworks/go/': '/integrations/frameworks/go/go-get-started/', + '/integrations/frameworks/go-apps/': '/integrations/frameworks/go/go-get-started/', + '/ja/integrations/frameworks/go-apps/': '/integrations/frameworks/go/go-get-started/', '/integrations/frameworks/csharp-file-based-apps/': '/integrations/dotnet/csharp-file-based-apps/', '/integrations/frameworks/maui/': '/integrations/dotnet/maui/', '/fundamentals/service-defaults/': '/get-started/csharp-service-defaults/', diff --git a/src/frontend/config/sidebar/docs.topics.ts b/src/frontend/config/sidebar/docs.topics.ts index 419aa2e62..7c644758b 100644 --- a/src/frontend/config/sidebar/docs.topics.ts +++ b/src/frontend/config/sidebar/docs.topics.ts @@ -90,6 +90,10 @@ export const docsTopics: StarlightSidebarTopicsUserConfig = { label: "What's new", collapsed: true, items: [ + { + label: 'Aspire 13.4', + slug: 'whats-new/aspire-13-4', + }, { label: 'Aspire 13.3', slug: 'whats-new/aspire-13-3', @@ -901,11 +905,11 @@ export const docsTopics: StarlightSidebarTopicsUserConfig = { collapsed: true, items: [ { - label: 'Persistent containers', - slug: 'app-host/persistent-containers', + label: 'Resource lifetimes', + slug: 'app-host/resource-lifetimes', translations: { - en: 'Persistent containers', - ja: '永続コンテナー', + en: 'Resource lifetimes', + ja: 'リソースのライフタイム', }, }, { diff --git a/src/frontend/config/sidebar/integrations.topics.ts b/src/frontend/config/sidebar/integrations.topics.ts index 6a845b82b..9dc395801 100644 --- a/src/frontend/config/sidebar/integrations.topics.ts +++ b/src/frontend/config/sidebar/integrations.topics.ts @@ -1248,7 +1248,7 @@ export const integrationTopics: StarlightSidebarTopicsUserConfig = { }, ], }, - { + { label: 'SQLite', collapsed: true, items: [ @@ -1332,13 +1332,35 @@ export const integrationTopics: StarlightSidebarTopicsUserConfig = { { label: 'C# file-based apps', slug: 'integrations/dotnet/csharp-file-based-apps' }, { label: 'Launch profiles', slug: 'integrations/dotnet/launch-profiles' }, { label: '.NET tool resources', slug: 'integrations/dotnet/dotnet-tool-resources' }, + { + label: 'Blazor', + collapsed: true, + items: [ + { label: 'Get started', slug: 'integrations/dotnet/blazor-get-started' }, + { label: 'Set up Blazor hosting in the AppHost', slug: 'integrations/dotnet/blazor-hosting' }, + { label: 'Connect Blazor apps and APIs', slug: 'integrations/dotnet/blazor-connect' }, + ], + }, { label: '.NET MAUI', slug: 'integrations/dotnet/maui' }, { label: 'WPF and Windows Forms', slug: 'integrations/frameworks/wpf-winforms' }, { label: 'Orleans', slug: 'integrations/frameworks/orleans' }, ], }, { label: 'Dapr', slug: 'integrations/frameworks/dapr' }, - { label: 'Go', slug: 'integrations/frameworks/go-apps' }, + { + label: 'Go', + collapsed: true, + items: [ + { + label: 'Get started', + slug: 'integrations/frameworks/go/go-get-started', + }, + { + label: 'Set up Go apps in the AppHost', + slug: 'integrations/frameworks/go/go-host', + }, + ], + }, { label: 'Java', slug: 'integrations/frameworks/java' }, { label: 'JavaScript and Node.js', diff --git a/src/frontend/config/sidebar/reference.topics.ts b/src/frontend/config/sidebar/reference.topics.ts index 91ec7435f..7f8024471 100644 --- a/src/frontend/config/sidebar/reference.topics.ts +++ b/src/frontend/config/sidebar/reference.topics.ts @@ -570,10 +570,6 @@ export const referenceTopics: StarlightSidebarTopicsUserConfig[number] = { { label: 'ASPIRE002', link: '/diagnostics/aspire002' }, { label: 'ASPIRE003', link: '/diagnostics/aspire003' }, { label: 'ASPIRE004', link: '/diagnostics/aspire004' }, - { - label: 'ASPIREATS001', - link: '/diagnostics/aspireats001', - }, { label: 'ASPIREEXPORT005', link: '/diagnostics/aspireexport005', @@ -602,6 +598,10 @@ export const referenceTopics: StarlightSidebarTopicsUserConfig[number] = { label: 'ASPIREEXPORT013', link: '/diagnostics/aspireexport013', }, + { + label: 'ASPIREEXPORT016', + link: '/diagnostics/aspireexport016', + }, { label: 'ASPIRECERTIFICATES001', link: '/diagnostics/aspirecertificates001', @@ -666,6 +666,10 @@ export const referenceTopics: StarlightSidebarTopicsUserConfig[number] = { label: 'ASPIREPOSTGRES001', link: '/diagnostics/aspirepostgres001', }, + { + label: 'ASPIREPROCESSCOMMAND001', + link: '/diagnostics/aspireprocesscommand001', + }, { label: 'ASPIREUSERSECRETS001', link: '/diagnostics/aspireusersecrets001', @@ -708,6 +712,10 @@ export const referenceTopics: StarlightSidebarTopicsUserConfig[number] = { label: 'ASPIREEXPORT004', link: '/diagnostics/aspireexport004', }, + { + label: 'ASPIREEXPORT015', + link: '/diagnostics/aspireexport015', + }, { label: 'ASPIREHOSTINGPYTHON001', link: '/diagnostics/aspirehostingpython001', diff --git a/src/frontend/config/twoslash.config.mjs b/src/frontend/config/twoslash.config.mjs index 0d1f09879..0ab1c1ac5 100644 --- a/src/frontend/config/twoslash.config.mjs +++ b/src/frontend/config/twoslash.config.mjs @@ -41,7 +41,8 @@ export const TWOSLASH_ENABLED = true; * `typescript` package from `ec.config.mjs`). * * - `moduleResolution: 100` → `ts.ModuleResolutionKind.Bundler` so - * `./.modules/aspire.js` falls through to the virtual `.modules/aspire.ts`. + * `./.aspire/modules/aspire.mjs` falls through to the virtual + * `.aspire/modules/aspire.mts`. * - `module: 99` → `ts.ModuleKind.ESNext` (paired with bundler resolution). * - `target: 99` → `ts.ScriptTarget.ESNext` so `lib.esnext.full.d.ts` is the * default `lib`, pulling in `Date`, `URL`, DOM, and friends via TS's @@ -79,12 +80,12 @@ export function readAspireTypes() { /** * Returns the `extraFiles` map twoslash should mount in its VFS. Returns an * empty object when the SDK bundle is missing so twoslash can still compile - * blocks that don't import from `./.modules/aspire.js` (they'll just see + * blocks that don't import from `./.aspire/modules/aspire.mjs` (they'll just see * `any` for the missing module — same fallback `ec.config.mjs` had inline). */ export function getTwoslashExtraFiles() { const { source } = readAspireTypes(); - return source ? { '.modules/aspire.ts': source } : {}; + return source ? { '.aspire/modules/aspire.mts': source } : {}; } /** diff --git a/src/frontend/scripts/generate-twoslash-types.ts b/src/frontend/scripts/generate-twoslash-types.ts index d5575b795..8cd874691 100644 --- a/src/frontend/scripts/generate-twoslash-types.ts +++ b/src/frontend/scripts/generate-twoslash-types.ts @@ -105,13 +105,10 @@ function cleanType(raw: string | undefined): string { // Collapse fully-qualified namespaced identifiers (with '.' and/or '/' separators) // to their trailing simple-name segment, in-place — so occurrences inside generic // args are handled without swallowing surrounding brackets. - s = s.replace( - /[A-Za-z_][A-Za-z0-9_]*(?:[./][A-Za-z_][A-Za-z0-9_]*)+/g, - (m) => { - const afterSlash = m.includes('/') ? m.slice(m.lastIndexOf('/') + 1) : m; - return lastDotted(afterSlash); - } - ); + s = s.replace(/[A-Za-z_][A-Za-z0-9_]*(?:[./][A-Za-z_][A-Za-z0-9_]*)+/g, (m) => { + const afterSlash = m.includes('/') ? m.slice(m.lastIndexOf('/') + 1) : m; + return lastDotted(afterSlash); + }); // recover from known junk produced by the upstream generator (stray `]]`) s = s.replace(/\]\]+/g, ''); return s || 'unknown'; @@ -130,8 +127,22 @@ function camelCase(name: string): string { function sanitizeIdentifier(name: string): string { // JS reserved words we might collide with when a param is named e.g. 'default' const reserved = new Set([ - 'default', 'function', 'class', 'new', 'return', 'delete', 'enum', 'package', - 'private', 'protected', 'public', 'static', 'interface', 'in', 'of', 'as' + 'default', + 'function', + 'class', + 'new', + 'return', + 'delete', + 'enum', + 'package', + 'private', + 'protected', + 'public', + 'static', + 'interface', + 'in', + 'of', + 'as', ]); return reserved.has(name) ? `_${name}` : name; } @@ -156,9 +167,19 @@ function formatParams(params: Parameter[]): string { // Primitive types we're safe to collapse into an options-object overload. If any // param uses a richer type (resource/handle/enum) we keep the positional form. const PRIMITIVE_TYPES = new Set([ - 'string', 'number', 'boolean', 'bigint', 'symbol', 'unknown', 'any', - 'string[]', 'number[]', 'boolean[]', - 'Array', 'Array', 'Array', + 'string', + 'number', + 'boolean', + 'bigint', + 'symbol', + 'unknown', + 'any', + 'string[]', + 'number[]', + 'boolean[]', + 'Array', + 'Array', + 'Array', ]); function isPrimitiveParamType(t: string): boolean { @@ -193,6 +214,14 @@ const PARAM_TYPE_OVERRIDES: Record> = { name: 'string | ParameterResource', resourceGroup: 'string | ParameterResource', }, + // Current Aspire TS SDKs accept connection-string resources as wait + // dependencies; the 13.3 ts-modules snapshot still reports IResource only. + waitFor: { + dependency: 'IResource | IResourceWithConnectionString', + }, + waitForStart: { + dependency: 'IResource | IResourceWithConnectionString', + }, }; // Global "broaden this type wherever it appears as a parameter" rules. The @@ -330,6 +359,107 @@ const genericArity = new Map(); const FREE_FUNCTION_NAMES = new Set(['createBuilder', 'createBuilderWithOptions']); +// Confirmed Aspire 13.4 API surface that may be absent from the checked-in +// 13.3 ts-modules snapshot until package data is refreshed. Keep these shims +// narrow and remove them when update-ts-api brings the APIs into +// src/data/ts-modules. +const POST_SNAPSHOT_FREE_FUNCTIONS = [ + `/** + * Creates a reference expression from a tagged template literal + */ +export declare function refExpr(strings: TemplateStringsArray, ...values: unknown[]): ReferenceExpression;`, +]; + +const POST_SNAPSHOT_DECLARATIONS = [ + `/** + * Enum Aspire.Hosting.ApplicationModel.InputType + */ +export type InputType = "Text" | "Number" | "Choice" | "SecretText"; +export declare const InputType: { + readonly Text: "Text"; + readonly Number: "Number"; + readonly Choice: "Choice"; + readonly SecretText: "SecretText"; +};`, + `export interface ParameterCustomInputOptions { + inputType?: InputType; + label?: string; + placeholder?: string; + options?: Record; +}`, + `export interface BeforePublishEvent extends IDistributedApplicationEvent { + model: PropertyAccessor; + services: PropertyAccessor; +}`, + `export interface AfterPublishEvent extends IDistributedApplicationEvent { + model: PropertyAccessor; + services: PropertyAccessor; +}`, +]; + +const POST_SNAPSHOT_AUGMENTATIONS = [ + `export interface IDistributedApplicationBuilder { + /** + * Subscribes to the BeforePublish event + */ + subscribeBeforePublish(callback: (arg: BeforePublishEvent) => Promise): DistributedApplicationEventSubscription; + /** + * Subscribes to the AfterPublish event + */ + subscribeAfterPublish(callback: (arg: AfterPublishEvent) => Promise): DistributedApplicationEventSubscription; +}`, + `export interface EventingSubscriberRegistrationContext { + /** + * Subscribes an eventing subscriber to the BeforePublish event + */ + onBeforePublish(callback: (arg: BeforePublishEvent) => Promise): DistributedApplicationEventSubscription; + /** + * Subscribes an eventing subscriber to the AfterPublish event + */ + onAfterPublish(callback: (arg: AfterPublishEvent) => Promise): DistributedApplicationEventSubscription; +}`, + `export interface IResource { + /** + * Assigns Microsoft Foundry roles for this resource + */ + withFoundryRoleAssignments(target: FoundryResource, roles: FoundryRole[]): this; + /** + * Configures a resource to use a session lifetime + */ + withSessionLifetime(): this; + /** + * Configures a resource to use a persistent lifetime + */ + withPersistentLifetime(): this; + /** + * Configures a resource to match the lifetime of another resource + */ + withLifetimeOf(source: IResource): this; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits + */ + withParentProcessLifetime(parentProcessId: number): this; +}`, + `export interface ParameterResource { + /** + * Customizes the parameter input shown by the dashboard + */ + withCustomInput(options: ParameterCustomInputOptions): this; +}`, + `export interface ComposeFile { + /** + * Adds a top-level Docker Compose volume + */ + addVolume(name: string, options?: { driver?: string; configure?: (volume: Volume) => Promise }): Promise; +}`, + `export interface Service { + /** + * Adds a Docker Compose volume mount to a service + */ + addVolume(source: string, target: string, options?: { isReadOnly?: boolean }): Promise; +}`, +]; + // Built-in generic container types the ATS tool emits as pseudo-targets — these // aren't useful in user-facing docs samples (Dict is already modeled as Record, // List as Array), so we skip them when assigning methods to target interfaces. @@ -388,9 +518,8 @@ for (const mod of modules) { function visitFn(fn: FunctionEntry): void { extractTypeIdentifiers(cleanType(fn.returnType), referencedTypes); for (const p of fn.parameters) { - const t = p.isCallback && p.callbackSignature - ? cleanType(p.callbackSignature) - : cleanType(p.type); + const t = + p.isCallback && p.callbackSignature ? cleanType(p.callbackSignature) : cleanType(p.type); extractTypeIdentifiers(t, referencedTypes); // track generic arity: look for Name<...> and count top-level commas const match = t.match(/([A-Za-z_][A-Za-z0-9_]*)<([^<>]*(?:<[^<>]*>[^<>]*)*)>/g); @@ -448,13 +577,40 @@ for (const fn of freeFunctions) scanExprForGenerics(cleanType(fn.returnType)); // ---------- emit ---------- const BUILT_IN = new Set([ - 'string', 'number', 'boolean', 'any', 'unknown', 'void', 'never', 'null', 'undefined', - 'object', 'true', 'false', 'this', 'symbol', 'bigint', + 'string', + 'number', + 'boolean', + 'any', + 'unknown', + 'void', + 'never', + 'null', + 'undefined', + 'object', + 'true', + 'false', + 'this', + 'symbol', + 'bigint', // JS globals we pass through - 'Promise', 'Array', 'Map', 'Set', 'Date', 'Error', 'RegExp', 'Record', 'Partial', - 'Readonly', 'Required', 'Pick', 'Omit', 'Exclude', 'Extract', + 'Promise', + 'Array', + 'Map', + 'Set', + 'Date', + 'Error', + 'RegExp', + 'Record', + 'Partial', + 'Readonly', + 'Required', + 'Pick', + 'Omit', + 'Exclude', + 'Extract', // Our utility aliases / internal helpers declared at the top of the file - 'Dict', 'PropertyAccessor', + 'Dict', + 'PropertyAccessor', ]); const declaredTypes = new Set([ @@ -467,7 +623,7 @@ const declaredTypes = new Set([ const parts: string[] = []; parts.push(`// Auto-generated by scripts/generate-twoslash-types.ts — do not edit.`); parts.push(`// This file is consumed by expressive-code-twoslash to provide hover tooltips`); -parts.push(`// for TypeScript code samples in the docs that import './.modules/aspire.js'.`); +parts.push(`// for TypeScript code samples in the docs that import './.aspire/modules/aspire.mjs'.`); parts.push(``); parts.push(`declare global {`); parts.push(` type Dict = Record;`); @@ -481,7 +637,9 @@ parts.push(`// \`builder.executionContext.isRunMode\` — nested direct parts.push(`// Model as an intersection covering all three. For object T we include`); parts.push(`// T directly so nested-property access resolves; for primitives we only`); parts.push(`// need the callable + accessor surface (primitive intersections collapse).`); -parts.push(`export type PropertyAccessor = (T extends object ? T : unknown) & (() => Promise) & {`); +parts.push( + `export type PropertyAccessor = (T extends object ? T : unknown) & (() => Promise) & {` +); parts.push(` get(): Promise;`); parts.push(` set(value: T): Promise;`); parts.push(` // Dict-valued accessors are sometimes set entry-wise in docs`); @@ -490,6 +648,10 @@ parts.push(` set(key: string, value: unknown): Promise;`); parts.push(`};`); parts.push(``); parts.push(`// ---- enums ----`); +for (const declaration of POST_SNAPSHOT_DECLARATIONS) { + parts.push(declaration); + parts.push(''); +} for (const en of enumTypes) { parts.push(jsdoc([`Enum ${en.fullName}`])); const members = en.members.map((m) => JSON.stringify(m)).join(' | ') || 'string'; @@ -499,9 +661,7 @@ for (const en of enumTypes) { // upstream dump only gives us string-valued members, which matches how the // generated SDK surfaces .NET enums to TS. if (en.members.length > 0) { - const fields = en.members - .map((m) => ` readonly ${m}: ${JSON.stringify(m)};`) - .join('\n'); + const fields = en.members.map((m) => ` readonly ${m}: ${JSON.stringify(m)};`).join('\n'); parts.push(`export declare const ${en.name}: {\n${fields}\n};`); } parts.push(''); @@ -547,9 +707,7 @@ function isContainerBacked(h: HandleType): boolean { if (NON_CONTAINER_HANDLES.has(h.name)) return false; const pkg = handlePackage.get(h.name); if (pkg && NON_CONTAINER_PACKAGES.has(pkg)) return false; - const ifaces = new Set( - (h.implementedInterfaces ?? []).map((i) => lastDotted(i).split('<')[0]) - ); + const ifaces = new Set((h.implementedInterfaces ?? []).map((i) => lastDotted(i).split('<')[0])); return ( ifaces.has('IComputeResource') && ifaces.has('IResourceWithArgs') && @@ -581,8 +739,7 @@ for (const h of handleTypes) { } ancestor = classBaseByName.get(ancestor); } - const implementsClause = - uniqueParents.length > 0 ? ` extends ${uniqueParents.join(', ')}` : ''; + const implementsClause = uniqueParents.length > 0 ? ` extends ${uniqueParents.join(', ')}` : ''; for (const i of uniqueParents) referencedTypes.add(i); parts.push(jsdoc([`Handle ${h.fullName}`])); parts.push(`export interface ${h.name}${implementsClause} {`); @@ -658,8 +815,7 @@ function emitMember(fn: FunctionEntry, indent: string, out: string[]): void { // polymorphic `this` so the type flows through. Applies to any method // flagged `returnsBuilder` that isn't actually creating a new resource. const isFluentPrefix = /^(with|wait|on|publish)[A-Z0-9]/.test(memberName); - const ret = - fn.returnsBuilder && isFluentPrefix ? 'this' : declaredRet; + const ret = fn.returnsBuilder && isFluentPrefix ? 'this' : declaredRet; const effectiveParams = applyParamOverrides(memberName, fn.parameters); // Record referenced types from overridden param types too, so the stubber sees them. for (const p of effectiveParams) extractTypeIdentifiers(paramType(p), referencedTypes); @@ -704,6 +860,16 @@ for (const fn of freeFunctions) { parts.push(`export declare function ${fn.name}(${formatParams(effectiveParams)}): ${ret};`); parts.push(''); } +for (const declaration of POST_SNAPSHOT_FREE_FUNCTIONS) { + parts.push(declaration); + parts.push(''); +} + +parts.push(`// ---- confirmed post-snapshot API augmentations ----`); +for (const declaration of POST_SNAPSHOT_AUGMENTATIONS) { + parts.push(declaration); + parts.push(''); +} // ---- stubs for referenced-but-undeclared types ---- const missing: string[] = []; @@ -727,7 +893,9 @@ for (const name of missing) { const extendsClause = /^IResourceWith/.test(name) && name !== 'IResource' ? ' extends IResource' : ''; if (arity > 0) { - const generics = Array.from({ length: arity }, (_, i) => `T${i === 0 ? '' : i} = unknown`).join(', '); + const generics = Array.from({ length: arity }, (_, i) => `T${i === 0 ? '' : i} = unknown`).join( + ', ' + ); parts.push(`export interface ${name}<${generics}>${extendsClause} {}`); } else { parts.push(`export interface ${name}${extendsClause} {}`); @@ -740,6 +908,10 @@ parts.push(`export {};`); mkdirSync(OUTPUT_DIR, { recursive: true }); writeFileSync(OUTPUT_FILE, parts.join('\n'), 'utf8'); console.log(`✅ Wrote ${OUTPUT_FILE}`); -console.log(` - ${dtoTypes.length} DTOs, ${enumTypes.length} enums, ${handleTypes.length} handle types`); -console.log(` - ${methodsByTarget.size} target interfaces, ${freeFunctions.length} free functions`); +console.log( + ` - ${dtoTypes.length} DTOs, ${enumTypes.length} enums, ${handleTypes.length} handle types` +); +console.log( + ` - ${methodsByTarget.size} target interfaces, ${freeFunctions.length} free functions` +); console.log(` - ${missing.length} stub interfaces for referenced SDK types`); diff --git a/src/frontend/scripts/update-integrations.ts b/src/frontend/scripts/update-integrations.ts index 193046c8a..14963e432 100644 --- a/src/frontend/scripts/update-integrations.ts +++ b/src/frontend/scripts/update-integrations.ts @@ -22,6 +22,7 @@ const EXCLUDED_PACKAGES = [ 'Aspire.RabbitMQ.Client.v7', 'CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps', 'CommunityToolkit.Aspire.Hosting.EventStore', + 'CommunityToolkit.Aspire.Hosting.Golang', 'CommunityToolkit.Aspire.EventStore', ]; const OUTPUT_PATH = './src/data/aspire-integrations.json'; diff --git a/src/frontend/src/components/AppHostBuilder.astro b/src/frontend/src/components/AppHostBuilder.astro index 1f5a00098..404a17370 100644 --- a/src/frontend/src/components/AppHostBuilder.astro +++ b/src/frontend/src/components/AppHostBuilder.astro @@ -445,7 +445,7 @@ builder.Build().Run();`, // Define all possible code combinations — TypeScript const tsCodes: Record = { - empty: `import { createBuilder } from './.modules/aspire.js'; + empty: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -455,7 +455,7 @@ const builder = await createBuilder(); await builder.build().run();`, - frontend: `import { createBuilder } from './.modules/aspire.js'; + frontend: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -466,7 +466,7 @@ const frontend = await builder await builder.build().run();`, - frontendContainer: `import { createBuilder } from './.modules/aspire.js'; + frontendContainer: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -482,7 +482,7 @@ const customContainer = await builder await builder.build().run();`, - databaseFrontend: `import { createBuilder } from './.modules/aspire.js'; + databaseFrontend: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -498,7 +498,7 @@ const frontend = await builder await builder.build().run();`, - databaseFrontendContainer: `import { createBuilder } from './.modules/aspire.js'; + databaseFrontendContainer: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -519,7 +519,7 @@ const customContainer = await builder await builder.build().run();`, - apiFrontend: `import { createBuilder } from './.modules/aspire.js'; + apiFrontend: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -536,7 +536,7 @@ const frontend = await builder await builder.build().run();`, - apiFrontendContainer: `import { createBuilder } from './.modules/aspire.js'; + apiFrontendContainer: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -558,7 +558,7 @@ const customContainer = await builder await builder.build().run();`, - databaseApiFrontend: `import { createBuilder } from './.modules/aspire.js'; + databaseApiFrontend: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -581,7 +581,7 @@ const frontend = await builder await builder.build().run();`, - databaseApiFrontendContainer: `import { createBuilder } from './.modules/aspire.js'; + databaseApiFrontendContainer: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -609,7 +609,7 @@ const customContainer = await builder await builder.build().run();`, - database: `import { createBuilder } from './.modules/aspire.js'; + database: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -620,7 +620,7 @@ const postgres = await builder await builder.build().run();`, - databaseContainer: `import { createBuilder } from './.modules/aspire.js'; + databaseContainer: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -636,7 +636,7 @@ const customContainer = await builder await builder.build().run();`, - api: `import { createBuilder } from './.modules/aspire.js'; + api: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -647,7 +647,7 @@ const api = await builder await builder.build().run();`, - apiContainer: `import { createBuilder } from './.modules/aspire.js'; + apiContainer: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -663,7 +663,7 @@ const customContainer = await builder await builder.build().run();`, - databaseApi: `import { createBuilder } from './.modules/aspire.js'; + databaseApi: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -680,7 +680,7 @@ const api = await builder await builder.build().run();`, - databaseApiContainer: `import { createBuilder } from './.modules/aspire.js'; + databaseApiContainer: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -702,7 +702,7 @@ const customContainer = await builder await builder.build().run();`, - container: `import { createBuilder } from './.modules/aspire.js'; + container: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -713,7 +713,7 @@ const customContainer = await builder await builder.build().run();`, - frontendDeployment: `import { createBuilder } from './.modules/aspire.js'; + frontendDeployment: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -726,7 +726,7 @@ const frontend = await builder await builder.build().run();`, - frontendContainerDeployment: `import { createBuilder } from './.modules/aspire.js'; + frontendContainerDeployment: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -745,7 +745,7 @@ const customContainer = await builder await builder.build().run();`, - databaseFrontendDeployment: `import { createBuilder } from './.modules/aspire.js'; + databaseFrontendDeployment: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -765,7 +765,7 @@ const frontend = await builder await builder.build().run();`, - databaseFrontendContainerDeployment: `import { createBuilder } from './.modules/aspire.js'; + databaseFrontendContainerDeployment: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -791,7 +791,7 @@ const customContainer = await builder await builder.build().run();`, - apiFrontendDeployment: `import { createBuilder } from './.modules/aspire.js'; + apiFrontendDeployment: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -811,7 +811,7 @@ const frontend = await builder await builder.build().run();`, - apiFrontendContainerDeployment: `import { createBuilder } from './.modules/aspire.js'; + apiFrontendContainerDeployment: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -837,7 +837,7 @@ const customContainer = await builder await builder.build().run();`, - databaseApiFrontendDeployment: `import { createBuilder } from './.modules/aspire.js'; + databaseApiFrontendDeployment: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -866,7 +866,7 @@ const frontend = await builder await builder.build().run();`, - databaseApiFrontendContainerDeployment: `import { createBuilder } from './.modules/aspire.js'; + databaseApiFrontendContainerDeployment: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -901,7 +901,7 @@ const customContainer = await builder await builder.build().run();`, - databaseDeployment: `import { createBuilder } from './.modules/aspire.js'; + databaseDeployment: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -914,7 +914,7 @@ const postgres = await builder await builder.build().run();`, - databaseContainerDeployment: `import { createBuilder } from './.modules/aspire.js'; + databaseContainerDeployment: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -933,7 +933,7 @@ const customContainer = await builder await builder.build().run();`, - apiDeployment: `import { createBuilder } from './.modules/aspire.js'; + apiDeployment: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -945,7 +945,7 @@ const api = await builder await builder.build().run();`, - apiContainerDeployment: `import { createBuilder } from './.modules/aspire.js'; + apiContainerDeployment: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -963,7 +963,7 @@ const customContainer = await builder await builder.build().run();`, - databaseApiDeployment: `import { createBuilder } from './.modules/aspire.js'; + databaseApiDeployment: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -984,7 +984,7 @@ const api = await builder await builder.build().run();`, - databaseApiContainerDeployment: `import { createBuilder } from './.modules/aspire.js'; + databaseApiContainerDeployment: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -1011,7 +1011,7 @@ const customContainer = await builder await builder.build().run();`, - containerDeployment: `import { createBuilder } from './.modules/aspire.js'; + containerDeployment: `import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -1215,7 +1215,7 @@ await builder.build().run();`, data-variant={key} style={key === 'frontend' ? '' : 'display: none;'} > - + )) } diff --git a/src/frontend/src/components/SimpleAppHostCode.shared.ts b/src/frontend/src/components/SimpleAppHostCode.shared.ts index fac55e695..e50a97b20 100644 --- a/src/frontend/src/components/SimpleAppHostCode.shared.ts +++ b/src/frontend/src/components/SimpleAppHostCode.shared.ts @@ -184,7 +184,7 @@ builder.Build().Run(); `; const csharpTypeScript = ` -import { createBuilder } from './.modules/aspire.js'; +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -207,7 +207,7 @@ await builder.build().run(); `; const pythonTypeScript = ` -import { createBuilder } from './.modules/aspire.js'; +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -231,7 +231,7 @@ await builder.build().run(); `; const nodejsTypeScript = ` -import { createBuilder } from './.modules/aspire.js'; +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); diff --git a/src/frontend/src/content.config.ts b/src/frontend/src/content.config.ts index 9bd2819f9..34c8b58bb 100644 --- a/src/frontend/src/content.config.ts +++ b/src/frontend/src/content.config.ts @@ -31,6 +31,18 @@ export const collections = { * site-wide `og-image.png` is used in social cards instead. */ og: z.boolean().optional(), + /** + * SEO-only title override. Used **verbatim** as the page's + * `og:title` and `twitter:title` (no `· Aspire` suffix is + * appended) so authors can tune the social-card title to the + * 50–60 character optimal range without bloating the visible + * `

` or sidebar label. Falls back to `title` when unset. + * + * Prefer rewriting the visible `title` when the natural H1 can + * accommodate the longer string. Use `seoTitle` only when the + * sidebar/H1 must stay short (commands, terse labels, etc.). + */ + seoTitle: z.string().optional(), /** * The date the release was published to NuGet. Used on What's New * pages to display the release date near the top of the page. diff --git a/src/frontend/src/content/docs/app-host/certificate-configuration.mdx b/src/frontend/src/content/docs/app-host/certificate-configuration.mdx index 57385d665..85ecdd4ec 100644 --- a/src/frontend/src/content/docs/app-host/certificate-configuration.mdx +++ b/src/frontend/src/content/docs/app-host/certificate-configuration.mdx @@ -1,6 +1,7 @@ --- title: Certificate configuration -description: Learn how to configure HTTPS endpoints and certificate trust for resources in Aspire to enable secure communication. +seoTitle: Aspire HTTPS certificate configuration for AppHost +description: Configure HTTPS endpoints and certificate trust for Aspire resources to enable secure local development, container-to-container TLS, and trusted browser connections. --- import { Aside, Tabs, TabItem } from '@astrojs/starlight/components'; @@ -72,11 +73,13 @@ aspire certs trust You may need to reload your profile or start a new terminal session for the change to take effect. -### Developer certificate for DCP communication (Windows) +### Developer certificate for DCP communication -By default, Aspire's internal Developer Control Plane (DCP) server uses an ephemeral localhost certificate it generates itself for TLS. On Windows, you can opt in to using your trusted Aspire developer certificate for DCP communication instead, which avoids trust issues caused by the ephemeral certificate not being in the system trust store. +By default, Aspire uses the ASP.NET Core developer certificate to secure communication with its internal Developer Control Plane (DCP) server. This replaces the ephemeral localhost certificate that DCP would otherwise generate itself, and avoids certificate trust errors caused by that certificate not being in the system trust store. -Set the `ASPIRE_DCP_USE_DEVELOPER_CERTIFICATE` environment variable to `true` in your AppHost's `launchSettings.json` or as a system/user environment variable: +If no trusted developer certificate is found, Aspire automatically falls back to DCP's ephemeral certificate. + +To opt out and use DCP's default ephemeral certificate instead, set `ASPIRE_DCP_USE_DEVELOPER_CERTIFICATE` to `false` in your AppHost's `launchSettings.json` or as an environment variable: ```json title="Properties/launchSettings.json" { @@ -84,23 +87,17 @@ Set the `ASPIRE_DCP_USE_DEVELOPER_CERTIFICATE` environment variable to `true` in "https": { "commandName": "Project", "environmentVariables": { - "ASPIRE_DCP_USE_DEVELOPER_CERTIFICATE": "true" + "ASPIRE_DCP_USE_DEVELOPER_CERTIFICATE": "false" } } } } ``` -When this setting is enabled: - -- Aspire checks for a trusted developer certificate. -- If a trusted certificate is found, it is used to secure the DCP server. -- If no trusted certificate is found, Aspire falls back to the DCP-generated ephemeral certificate. -- This setting is only supported on Windows. On other platforms, a warning is logged and DCP falls back to its default ephemeral certificate. - ## HTTPS endpoint configuration @@ -135,8 +132,8 @@ builder.Build().Run(); ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -217,8 +214,8 @@ builder.Build().Run(); ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -333,8 +330,8 @@ builder.Build().Run(); ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -416,8 +413,8 @@ builder.Build().Run(); ``` -```typescript title="apphost.ts" twoslash -import { createBuilder, CertificateTrustScope } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder, CertificateTrustScope } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -473,8 +470,8 @@ builder.Build().Run(); ``` -```typescript title="apphost.ts" twoslash -import { createBuilder, CertificateTrustScope } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder, CertificateTrustScope } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -504,8 +501,8 @@ builder.Build().Run(); ``` -```typescript title="apphost.ts" twoslash -import { createBuilder, CertificateTrustScope } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder, CertificateTrustScope } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -621,8 +618,8 @@ builder.Build().Run(); ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -687,8 +684,8 @@ builder.Build().Run(); ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -723,8 +720,8 @@ builder.Build().Run(); ``` -```typescript title="apphost.ts" twoslash -import { createBuilder, CertificateTrustScope } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder, CertificateTrustScope } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); diff --git a/src/frontend/src/content/docs/app-host/configuration.mdx b/src/frontend/src/content/docs/app-host/configuration.mdx index 4d665aef4..c66c3dc32 100644 --- a/src/frontend/src/content/docs/app-host/configuration.mdx +++ b/src/frontend/src/content/docs/app-host/configuration.mdx @@ -1,6 +1,7 @@ --- title: AppHost configuration -description: Learn about the Aspire AppHost configuration options. +seoTitle: Aspire AppHost configuration reference and overview +description: Configure the Aspire AppHost — environment variables, launch profiles, network ports, container runtime selection, and the options that change orchestration behavior. --- import { Aside, Steps, Tabs, TabItem } from '@astrojs/starlight/components'; @@ -51,7 +52,7 @@ In TypeScript AppHosts, profiles live in `aspire.config.json`: ```json title="aspire.config.json" { "appHost": { - "path": "apphost.ts", + "path": "apphost.mts", "language": "typescript/nodejs" }, "profiles": { @@ -79,7 +80,7 @@ In TypeScript AppHosts, profiles live in `aspire.config.json`: | ---------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ASPIRE_ALLOW_UNSECURED_TRANSPORT` | `false` | Allows communication with the AppHost without https. `ASPNETCORE_URLS` (dashboard address) and `ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL` (AppHost resource service address) must be secured with HTTPS unless true. | | `ASPIRE_CONTAINER_RUNTIME` | `docker` | Allows the user of alternative container runtimes for resources backed by containers. Possible values are `docker` (default) or `podman`. | -| `ASPIRE_DCP_USE_DEVELOPER_CERTIFICATE` | `false` | When set to `true`, Aspire uses its trusted developer certificate to secure the internal DCP server instead of an ephemeral certificate generated by DCP. This can help avoid certificate trust issues when the dev cert is already trusted. If no trusted developer certificate is found, Aspire falls back to the DCP-generated ephemeral certificate. Only supported on Windows. For more information, see [Certificate configuration](/app-host/certificate-configuration/). | +| `ASPIRE_DCP_USE_DEVELOPER_CERTIFICATE` | `true` | When `true` (the default), Aspire uses the ASP.NET Core developer certificate to secure the internal DCP server instead of an ephemeral certificate generated by DCP. On Windows, Aspire passes the certificate thumbprint to DCP. On macOS and Linux, Aspire passes the certificate and private key file paths (plus the thumbprint) so DCP can verify the loaded certificate. Set to `false` to opt out and use DCP's default ephemeral certificate. If no trusted developer certificate is found, Aspire automatically falls back to the ephemeral certificate. For more information, see [Certificate configuration](/app-host/certificate-configuration/). | | `ASPIRE_ENVIRONMENT` | `null` | Configures the AppHost environment when no higher-priority environment source is set. If no environment is configured, the AppHost uses `Production`. | | `ASPIRE_VERSION_CHECK_DISABLED` | `false` | When set to `true`, Aspire doesn't check for newer versions on startup. | diff --git a/src/frontend/src/content/docs/app-host/container-files.mdx b/src/frontend/src/content/docs/app-host/container-files.mdx index ea2186027..ba1dab14c 100644 --- a/src/frontend/src/content/docs/app-host/container-files.mdx +++ b/src/frontend/src/content/docs/app-host/container-files.mdx @@ -1,6 +1,7 @@ --- title: Container files -description: Learn how to inject files and directories into containers at development time and publish time using the container file APIs in Aspire. +seoTitle: Inject container files in your Aspire AppHost project +description: Inject files and directories into Aspire container resources at development and publish time using WithContainerFiles, with options for source paths and permissions. --- import { Aside, Tabs, TabItem } from '@astrojs/starlight/components'; @@ -346,8 +347,8 @@ builder.Build().Run(); ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -385,8 +386,8 @@ builder.Build().Run(); ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -431,4 +432,4 @@ Use `ClearContainerFilesSources` to remove any previously configured source path - [Certificate configuration](/app-host/certificate-configuration/) - [Add Dockerfiles to your app model](/app-host/withdockerfile/) -- [Persistent container lifetimes](/app-host/persistent-containers/) +- [Resource lifetimes](/app-host/resource-lifetimes/) diff --git a/src/frontend/src/content/docs/app-host/container-registry.mdx b/src/frontend/src/content/docs/app-host/container-registry.mdx index f93eefb79..3a8efc658 100644 --- a/src/frontend/src/content/docs/app-host/container-registry.mdx +++ b/src/frontend/src/content/docs/app-host/container-registry.mdx @@ -1,6 +1,7 @@ --- title: Container registry configuration -description: Learn how to configure container registries for your Aspire applications, including generic registries and Azure Container Registry. +seoTitle: Configure container registries for your Aspire AppHost +description: Configure container registries for Aspire — generic registries, Docker Hub, Azure Container Registry, GitHub Container Registry, and per-resource image tagging. --- import { Aside, Steps, Tabs, TabItem } from '@astrojs/starlight/components'; @@ -45,8 +46,8 @@ builder.Build().Run(); ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -84,8 +85,8 @@ var api = builder.AddProject("api") ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -113,8 +114,8 @@ var api = builder.AddProject("api") ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -144,8 +145,8 @@ var api = builder.AddProject("api") ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -185,8 +186,8 @@ var api = builder.AddProject("api") ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -270,8 +271,8 @@ builder.Build().Run(); ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -323,8 +324,8 @@ var api = builder.AddProject("api") ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -426,8 +427,8 @@ var internalApi = builder.AddProject("internal-api") ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -472,8 +473,8 @@ builder.Build().Run(); ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -543,8 +544,8 @@ builder.Build().Run(); ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); diff --git a/src/frontend/src/content/docs/app-host/docker-compose-to-apphost-reference.mdx b/src/frontend/src/content/docs/app-host/docker-compose-to-apphost-reference.mdx index 999e63780..fa440161d 100644 --- a/src/frontend/src/content/docs/app-host/docker-compose-to-apphost-reference.mdx +++ b/src/frontend/src/content/docs/app-host/docker-compose-to-apphost-reference.mdx @@ -1,6 +1,6 @@ --- -title: Docker Compose to Aspire AppHost -description: Quick reference for converting Docker Compose YAML syntax to Aspire AppHost API calls. +title: Docker Compose to Aspire AppHost reference +description: Quick reference for converting Docker Compose YAML syntax to Aspire AppHost API calls — services, networks, volumes, environment variables, and health checks. --- import LearnMore from '@components/LearnMore.astro'; diff --git a/src/frontend/src/content/docs/app-host/eventing.mdx b/src/frontend/src/content/docs/app-host/eventing.mdx index deaae65d9..7afd6e2c8 100644 --- a/src/frontend/src/content/docs/app-host/eventing.mdx +++ b/src/frontend/src/content/docs/app-host/eventing.mdx @@ -1,6 +1,7 @@ --- title: AppHost eventing APIs -description: Learn how to use the Aspire AppHost eventing features for lifecycle events, custom event publishing, and event-driven resource orchestration. +seoTitle: AppHost eventing APIs in your Aspire AppHost project +description: Use the Aspire AppHost eventing APIs for lifecycle events, custom event publishing, and reactive integrations that respond to resource state transitions at runtime. --- import { Aside, Steps, Tabs, TabItem } from '@astrojs/starlight/components'; @@ -16,7 +17,7 @@ The following events are available in the AppHost and occur in the following ord 1. `BeforeStartEvent`: This event is raised before the AppHost starts. 1. `ResourceEndpointsAllocatedEvent`: This event is raised per resource after its endpoints are allocated. -1. `AfterResourcesCreatedEvent`: This event is raised after the AppHost created resources. +1. `AfterResourcesCreatedEvent`: This event is raised after resources are created. @@ -24,8 +25,9 @@ The following events are available in the AppHost and occur in the following ord To subscribe to built-in AppHost events, use the convenience extension methods directly on the builder. These methods return the same `IDistributedApplicationBuilder` instance so calls can be chained: - - + + + ```csharp title="AppHost.cs" using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -39,16 +41,46 @@ builder.OnBeforeStart(static (@event, cancellationToken) => return Task.CompletedTask; }); +builder.OnAfterResourcesCreated(static (@event, cancellationToken) => +{ + var logger = @event.Services.GetRequiredService>(); + logger.LogInformation("AfterResourcesCreatedEvent"); + return Task.CompletedTask; +}); + builder.Build().Run(); ``` + + + +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +await builder.subscribeBeforeStart(async () => { + console.log('BeforeStartEvent'); +}); + +await builder.subscribeAfterResourcesCreated(async () => { + console.log('AfterResourcesCreatedEvent'); +}); + +await builder.build().run(); +``` + + + + The following builder-level extension methods are available for AppHost events: -| Method | Event | -|--------|-------| -| `OnBeforeStart` | `BeforeStartEvent` — raised before the AppHost starts | -| `OnBeforePublish` | `BeforePublishEvent` — raised before manifest publishing begins | -| `OnAfterPublish` | `AfterPublishEvent` — raised after manifest publishing completes | +| Method | Event | +| ------------------------- | ----------------------------------------------------------------- | +| `OnBeforeStart` | `BeforeStartEvent` — raised before the AppHost starts | +| `OnAfterResourcesCreated` | `AfterResourcesCreatedEvent` — raised after resources are created | +| `OnBeforePublish` | `BeforePublishEvent` — raised before manifest publishing begins | +| `OnAfterPublish` | `AfterPublishEvent` — raised after manifest publishing completes | If you need to subscribe via `IDistributedApplicationEventing` directly (for example, inside an `IDistributedApplicationEventingSubscriber`), you can use the lower-level `Eventing.Subscribe()` API: @@ -67,61 +99,39 @@ builder.Eventing.Subscribe( var logger = @event.Services.GetRequiredService>(); logger.LogInformation("AfterResourcesCreatedEvent"); return Task.CompletedTask; - -builder.Build().Run(); + }); ``` - - -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; - -const builder = await createBuilder(); -const cache = await builder.addRedis("cache"); - -await builder.subscribeBeforeStart(async (event) => { - console.log("BeforeStartEvent"); -}); - -await builder.subscribeAfterResourcesCreated(async (event) => { - console.log("AfterResourcesCreatedEvent"); -}); - -await builder.build().run(); -``` - - + When the AppHost is run, by the time the Aspire dashboard is displayed, you should see the following log output in the console: -```plaintext {2,10,12,14,16,22} data-disable-copy +```plaintext {2,12} data-disable-copy info: Program[0] - 1. BeforeStartEvent + BeforeStartEvent info: Aspire.Hosting.DistributedApplication[0] Aspire version: 13.1.0 info: Aspire.Hosting.DistributedApplication[0] Distributed application starting. info: Aspire.Hosting.DistributedApplication[0] Application host directory is: ../AspireApp/AspireApp.AppHost -info: Program[0] - 2. "cache" ResourceEndpointsAllocatedEvent -info: Program[0] - 2. "apiservice" ResourceEndpointsAllocatedEvent -info: Program[0] - 2. "webfrontend" ResourceEndpointsAllocatedEvent -info: Program[0] - 2. "aspire-dashboard" ResourceEndpointsAllocatedEvent info: Aspire.Hosting.DistributedApplication[0] Now listening on: https://localhost:17178 info: Aspire.Hosting.DistributedApplication[0] Login to the dashboard at https://localhost:17178/login?t= info: Program[0] - 3. AfterResourcesCreatedEvent + AfterResourcesCreatedEvent info: Aspire.Hosting.DistributedApplication[0] Distributed application started. Press Ctrl+C to shut down. ``` -The log output confirms that event handlers are executed in the order of the AppHost life cycle events. The subscription order doesn't affect execution order. The `BeforeStartEvent` is triggered first, followed by each resource's `ResourceEndpointsAllocatedEvent`, and finally `AfterResourcesCreatedEvent`. +The log output confirms that event handlers are executed in the order of the AppHost life cycle events. The subscription order doesn't affect execution order. The `BeforeStartEvent` is triggered before the AppHost starts, and `AfterResourcesCreatedEvent` is triggered after resources are created. ## Resource eventing @@ -139,7 +149,10 @@ In addition to the AppHost events, you can also subscribe to resource events. Re ### Subscribe to resource events -To subscribe to resource events, use the convenience-based extension methods—`On*`. After you have a distributed application builder instance, and a resource builder, walk up to the instance and chain a call to the desired `On*` event API. Consider the following sample _AppHost.cs_ file: +To subscribe to resource events, use the convenience-based extension methods. After you have a distributed application builder instance, and a resource builder, walk up to the instance and chain a call to the desired event API: + + + ```csharp title="AppHost.cs" using Microsoft.Extensions.DependencyInjection; @@ -206,13 +219,58 @@ builder.AddProject("webfrontend") builder.Build().Run(); ``` -The preceding code subscribes to the `InitializeResourceEvent`, `ResourceReadyEvent`, `ResourceEndpointsAllocatedEvent`, `ConnectionStringAvailableEvent`, and `BeforeResourceStartedEvent` events on the `cache` resource. When `AddRedis` is called, it returns an `IResourceBuilder` where `T` is a `RedisResource`. Chain calls to the `On*` methods to subscribe to the events. The `On*` methods return the same `IResourceBuilder` instance, so you can chain multiple calls: + + -- `OnInitializeResource`: Subscribes to the `InitializeResourceEvent`. -- `OnResourceEndpointsAllocated`: Subscribes to the `ResourceEndpointsAllocatedEvent` event. -- `OnConnectionStringAvailable`: Subscribes to the `ConnectionStringAvailableEvent` event. -- `OnBeforeResourceStarted`: Subscribes to the `BeforeResourceStartedEvent` event. -- `OnResourceReady`: Subscribes to the `ResourceReadyEvent` event. +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const cache = await builder.addRedis('cache'); + +await cache.onInitializeResource(async () => { + console.log('1. onInitializeResource'); +}); + +await cache.onResourceEndpointsAllocated(async (event) => { + const resource = await event.resource(); + console.log(`2. endpoints allocated for ${resource.getResourceName()}`); +}); + +await cache.onConnectionStringAvailable(async (event) => { + const resource = await event.resource(); + console.log( + `3. connection string available for ${resource.getResourceName()}` + ); +}); + +await cache.onBeforeResourceStarted(async () => { + console.log('4. onBeforeResourceStarted'); +}); + +await cache.onResourceReady(async () => { + console.log('5. onResourceReady'); +}); + +await builder.build().run(); +``` + + + + +The preceding code subscribes to the `InitializeResourceEvent`, `ResourceReadyEvent`, `ResourceEndpointsAllocatedEvent`, `ConnectionStringAvailableEvent`, and `BeforeResourceStartedEvent` events on the `cache` resource. Chain calls to the event methods to subscribe to multiple events on the same resource. + + + +- `OnInitializeResource` / `onInitializeResource`: Subscribes to the `InitializeResourceEvent`. +- `OnResourceEndpointsAllocated` / `onResourceEndpointsAllocated`: Subscribes to the `ResourceEndpointsAllocatedEvent` event. +- `OnConnectionStringAvailable` / `onConnectionStringAvailable`: Subscribes to the `ConnectionStringAvailableEvent` event. +- `OnBeforeResourceStarted` / `onBeforeResourceStarted`: Subscribes to the `BeforeResourceStartedEvent` event. +- `OnResourceReady` / `onResourceReady`: Subscribes to the `ResourceReadyEvent` event. When the AppHost is run, by the time the Aspire dashboard is displayed, you should see the following log output in the console: @@ -242,7 +300,11 @@ info: Aspire.Hosting.DistributedApplication[0] ``` ## Publish events @@ -312,6 +374,30 @@ internal sealed class LifecycleLoggerSubscriber(ILogger + C# eventing subscriber service classes are specific to the C# AppHost hosting + model. TypeScript AppHosts can register callback-based subscribers with + `addEventingSubscriber` or `tryAddEventingSubscriber`. + + +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +builder.addEventingSubscriber(async (events) => { + events.onBeforeStart(async () => { + console.log('1. BeforeStartEvent'); + }); + + events.onAfterResourcesCreated(async () => { + console.log('3. AfterResourcesCreatedEvent'); + }); +}); + +await builder.build().run(); +``` + The subscriber approach keeps builder code minimal while still letting you respond to the same lifecycle moments as inline subscriptions: - `AddEventingSubscriber()` (or `TryAddEventingSubscriber()`) ensures the subscriber participates whenever the AppHost starts. @@ -324,12 +410,12 @@ Use this pattern whenever you previously relied on `IDistributedApplicationLifec If you're migrating from the deprecated `IDistributedApplicationLifecycleHook` interface, use the following mapping: -| Old pattern (deprecated) | New pattern | -|--------------------------|-------------| -| `BeforeStartAsync()` | Subscribe to `BeforeStartEvent` | +| Old pattern (deprecated) | New pattern | +| -------------------------------- | ---------------------------------------------- | +| `BeforeStartAsync()` | Subscribe to `BeforeStartEvent` | | `AfterEndpointsAllocatedAsync()` | Subscribe to `ResourceEndpointsAllocatedEvent` | -| `AfterResourcesCreatedAsync()` | Subscribe to `AfterResourcesCreatedEvent` | -| `TryAddLifecycleHook()` | `TryAddEventingSubscriber()` | +| `AfterResourcesCreatedAsync()` | Subscribe to `AfterResourcesCreatedEvent` | +| `TryAddLifecycleHook()` | `TryAddEventingSubscriber()` | **Before (deprecated):** @@ -374,7 +460,9 @@ builder.Services.TryAddEventingSubscriber(); ``` ## Additional events @@ -385,10 +473,13 @@ Beyond the core lifecycle events, Aspire provides additional events for specific When publishing your application (generating deployment manifests), these events are raised: -| Event | When raised | Purpose | -|-------|-------------|---------| -| `BeforePublishEvent` | Before publishing begins | Validate or modify resources before manifest generation | -| `AfterPublishEvent` | After publishing completes | Perform cleanup or post-publish actions | +| Event | When raised | Purpose | +| -------------------- | -------------------------- | ------------------------------------------------------- | +| `BeforePublishEvent` | Before publishing begins | Validate or modify resources before manifest generation | +| `AfterPublishEvent` | After publishing completes | Perform cleanup or post-publish actions | + + + ```csharp title="AppHost.cs" builder.OnBeforePublish((@event, ct) => @@ -404,14 +495,40 @@ builder.OnAfterPublish((@event, ct) => }); ``` + + + +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +await builder.subscribeBeforePublish(async (event) => { + const model = await event.model(); + console.log(`Publishing ${model.getResources().length} resources`); +}); + +await builder.subscribeAfterPublish(async (event) => { + const services = await event.services(); + console.log('Publish completed', services); +}); +``` + + + + -For details on what happens during publishing, see [Publishing and deployment overview](/deployment/deploy-with-aspire/). + For details on what happens during publishing, see [Publishing and deployment + overview](/deployment/deploy-with-aspire/). ### Resource stopped event The `ResourceStoppedEvent` is raised when a resource stops execution: + + + ```csharp title="AppHost.cs" builder.Eventing.Subscribe( cache, @@ -422,8 +539,29 @@ builder.Eventing.Subscribe( }); ``` + + + +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const cache = await builder.addRedis('cache'); + +await cache.onResourceStopped(async (event) => { + const resource = await event.resource(); + console.log(`Resource ${resource.getResourceName()} stopped`); +}); +``` + + + + ## See also diff --git a/src/frontend/src/content/docs/app-host/executable-resources.mdx b/src/frontend/src/content/docs/app-host/executable-resources.mdx index 036fec721..505629e87 100644 --- a/src/frontend/src/content/docs/app-host/executable-resources.mdx +++ b/src/frontend/src/content/docs/app-host/executable-resources.mdx @@ -1,6 +1,6 @@ --- title: Host external executables in Aspire -description: Learn how to host external executable applications in your Aspire AppHost using AddExecutable. +description: Host external executable applications in your Aspire AppHost using AddExecutable — model CLI tools, daemons, and language runtimes alongside containers and projects. --- import { Aside, Steps, Tabs, TabItem } from '@astrojs/starlight/components'; @@ -43,8 +43,8 @@ builder.Build().Run(); ``` -```typescript title="apphost.ts" -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -78,8 +78,8 @@ var app = builder.AddExecutable( ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -107,8 +107,8 @@ var app = builder.AddExecutable("worker", "python", ".", "worker.py") ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -146,8 +146,8 @@ var app = builder.AddExecutable("app", "node", ".", "app.js") ``` -```typescript title="apphost.ts" twoslash -import { createBuilder, EndpointProperty } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder, EndpointProperty } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -187,8 +187,8 @@ builder.Build().Run(); ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -225,8 +225,8 @@ var e2eTests = builder.AddExecutable("playwright", "npx", ".", "playwright", "te ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -261,8 +261,8 @@ var app = builder.AddExecutable( ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -289,8 +289,8 @@ var app = builder.AddExecutable( ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -328,8 +328,8 @@ var app = builder.AddExecutable("frontend", "npm", ".", "start") ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); diff --git a/src/frontend/src/content/docs/app-host/hot-reload-and-watch.mdx b/src/frontend/src/content/docs/app-host/hot-reload-and-watch.mdx index cee1742e2..8b65e3886 100644 --- a/src/frontend/src/content/docs/app-host/hot-reload-and-watch.mdx +++ b/src/frontend/src/content/docs/app-host/hot-reload-and-watch.mdx @@ -1,6 +1,7 @@ --- title: Hot Reload and watch -description: Learn how hot reload works in Aspire. +seoTitle: Aspire AppHost hot reload and aspire watch overview +description: "Learn how hot reload works in Aspire and how `aspire watch` rebuilds and restarts resources automatically when project files change during development." --- import { Tabs, TabItem } from '@astrojs/starlight/components'; @@ -78,7 +79,7 @@ TypeScript AppHost watch doesn't automatically provide hot reload for every reso Use this workflow when changes affect: -- The AppHost model in `apphost.ts`. +- The AppHost model in `apphost.mts`. - Resource configuration, endpoints, parameters, or integration setup. - Multiple services that need to be restarted together under Aspire orchestration. diff --git a/src/frontend/src/content/docs/app-host/migrate-from-docker-compose.mdx b/src/frontend/src/content/docs/app-host/migrate-from-docker-compose.mdx index cb8b862ea..79f109372 100644 --- a/src/frontend/src/content/docs/app-host/migrate-from-docker-compose.mdx +++ b/src/frontend/src/content/docs/app-host/migrate-from-docker-compose.mdx @@ -1,6 +1,6 @@ --- title: Migrate from Docker Compose to Aspire -description: Learn how to migrate your Docker Compose applications to Aspire and understand the key conceptual differences. +description: Migrate your Docker Compose applications to Aspire — map services, volumes, networks, and environment variables to AppHost APIs and modernize your developer workflow. --- import { Aside, Steps, Tabs, TabItem } from '@astrojs/starlight/components'; @@ -124,8 +124,8 @@ builder.Build().Run(); ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -238,8 +238,8 @@ builder.Build().Run(); ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -329,8 +329,8 @@ builder.Build().Run(); ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -382,7 +382,7 @@ var app = builder.AddContainer("app", "myapp", "latest") ``` -```typescript title="apphost.ts" +```typescript title="apphost.mts" const dbPassword = builder.addParameter("dbPassword", { secret: true }); const db = (await builder.addPostgres("db", { password: dbPassword })) @@ -440,8 +440,8 @@ builder.Build().Run(); ``` -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -578,7 +578,7 @@ var app = builder.AddContainer("app", "myapp", "latest") ``` -```typescript title="apphost.ts" +```typescript title="apphost.mts" const dbPassword = builder.addParameter("dbPassword", { secret: true }); const postgres = (await builder.addPostgres("db", { password: dbPassword })) @@ -607,7 +607,7 @@ var api = builder.AddProject("api") ``` -```typescript title="apphost.ts" +```typescript title="apphost.mts" const api = await builder.addProject("api", "./Api/Api.csproj", "https") .withReference(database) // Service discovery .waitFor(database); // Startup ordering @@ -635,7 +635,7 @@ var redis = builder.AddRedis("cache") ``` -```typescript title="apphost.ts" +```typescript title="apphost.mts" const redis = await builder.addRedis("cache") .withHostPort(6379); ``` @@ -656,7 +656,7 @@ var api = builder.AddProject("api") ``` -```typescript title="apphost.ts" +```typescript title="apphost.mts" const api = await builder.addProject("api", "./Api/Api.csproj", "https") .withHttpHealthCheck("/health"); ``` @@ -685,7 +685,7 @@ var app = builder.AddProject("app") ``` -```typescript title="apphost.ts" +```typescript title="apphost.mts" const rabbit = await builder.addContainer("rabbitmq", "rabbitmq", "4.1.4-management-alpine") .withHealthCheck("rabbitmq-health"); diff --git a/src/frontend/src/content/docs/app-host/persistent-containers.mdx b/src/frontend/src/content/docs/app-host/persistent-containers.mdx deleted file mode 100644 index ed4f97630..000000000 --- a/src/frontend/src/content/docs/app-host/persistent-containers.mdx +++ /dev/null @@ -1,191 +0,0 @@ ---- -title: Persistent container lifetimes -description: Learn how to configure containers to persist and be re-used between Aspire AppHost runs. ---- - -import { Aside, Tabs, TabItem } from '@astrojs/starlight/components'; -import { Image } from 'astro:assets'; -import persistentContainer from '@assets/whats-new/aspire-9/persistent-container.png'; -import persistentContainerDocker from '@assets/whats-new/aspire-9/persistent-container-docker-desktop.png'; - -In Aspire, containers follow a typical lifecycle where they're created when the AppHost starts and destroyed when it stops. However, you can specify that you want to use **persistent containers**, which deviate from this standard lifecycle. Persistent containers are created and started by the Aspire orchestrator but aren't destroyed when the AppHost stops, allowing them to persist between runs. - -This feature is particularly beneficial for containers that have long startup times, such as databases, as it eliminates the need to wait for these services to initialize on every AppHost restart. - - - -## Configure a persistent container - -To configure a container resource with a persistent lifetime, use the `WithLifetime` method and pass `ContainerLifetime.Persistent`: - - - -```csharp title="AppHost.cs" -var builder = DistributedApplication.CreateBuilder(args); - -var postgres = builder.AddPostgres("postgres") - .WithLifetime(ContainerLifetime.Persistent) - .WithDataVolume(); - -var db = postgres.AddDatabase("inventorydb"); - -builder.AddProject("inventory") - .WithReference(db); - -builder.Build().Run(); -``` - - -```typescript title="apphost.ts" twoslash -import { createBuilder, ContainerLifetime } from './.modules/aspire.js'; - -const builder = await createBuilder(); - -const postgres = await builder.addPostgres("postgres") - .withLifetime(ContainerLifetime.Persistent) - .withDataVolume(); - -const db = postgres.addDatabase("inventorydb"); - -await builder.addProject("inventory", "./InventoryService/InventoryService.csproj") - .withReference(db); - -await builder.build().run(); -``` - - - -In the preceding example, the PostgreSQL container is configured to persist between AppHost runs, and `WithDataVolume()` ensures database data is stored in a named volume that survives container recreation. The `inventory` project references the database as normal. - -## Dashboard visualization - -The Aspire dashboard shows persistent containers with a distinctive pin icon (📌) to help you identify them: - -Screenshot of the Aspire dashboard showing a persistent container with a pin icon. - -After the AppHost stops, persistent containers continue running and can be seen in your container runtime (such as Docker Desktop): - -Screenshot of Docker Desktop showing a persistent RabbitMQ container still running after the AppHost stopped. - -## Configuration change detection - -Persistent containers are automatically recreated when the AppHost detects meaningful configuration changes. Aspire tracks a hash of the configuration used to create each container and compares it to the current configuration on subsequent runs. If the configuration differs, the container is recreated with the new settings. - -This mechanism ensures that persistent containers stay synchronized with your AppHost configuration without requiring manual intervention. - -## Container naming and uniqueness - -By default, persistent containers use a naming pattern that combines: - -- The service name you specify in your AppHost. -- A postfix based on a hash of the AppHost project path. - -This naming scheme ensures that persistent containers are unique to each AppHost project, preventing conflicts when multiple Aspire projects use the same service names. - -For example, if you have a service named `"postgres"` in an AppHost project located at `/path/to/MyApp.AppHost`, the container name might be `postgres-abc123def` where `abc123def` is derived from the project path hash. - -### Custom container names - -For advanced scenarios, you can set a custom container name using the `WithContainerName` method: - - - -```csharp title="AppHost.cs" -var builder = DistributedApplication.CreateBuilder(args); - -var postgres = builder.AddPostgres("postgres") - .WithLifetime(ContainerLifetime.Persistent) - .WithContainerName("my-shared-postgres"); - -builder.Build().Run(); -``` - - -```typescript title="apphost.ts" twoslash -import { createBuilder, ContainerLifetime } from './.modules/aspire.js'; - -const builder = await createBuilder(); - -const postgres = await builder.addPostgres("postgres") - .withLifetime(ContainerLifetime.Persistent) - .withContainerName("my-shared-postgres"); - -await builder.build().run(); -``` - - - -When you specify a custom container name, Aspire first checks if a container with that name already exists. If a container with that name exists and was previously created by Aspire, it follows the normal persistent container behavior and can be automatically recreated if the configuration changes. If a container with that name exists but wasn't created by Aspire, it won't be managed or recreated by the AppHost. If no container with the custom name exists, Aspire creates a new one. - -## Manual cleanup - - - -You can clean up persistent containers using Docker CLI commands: - -```bash -# Stop the container -docker stop my-container-name - -# Remove the container -docker rm my-container-name -``` - -Alternatively, you can use Docker Desktop or your preferred container management tool to stop and remove persistent containers. - -## Use cases and benefits - -Persistent containers are ideal for: - -- **Database services**: PostgreSQL, SQL Server, MySQL, and other databases that take time to initialize and load data. -- **Message brokers**: RabbitMQ, Redis, and similar services that benefit from maintaining state between runs. -- **Development data**: Containers with test data or configurations that you want to preserve during development iterations. -- **Shared services**: Services that multiple AppHosts or development team members can share. - -## Container lifetime vs. data durability - -`ContainerLifetime.Persistent` and `WithDataVolume()` serve different purposes and are often used together. The following table summarizes the behavior of each combination: - -| Configuration | Container behavior | Data behavior | -|---|---|---| -| Neither (default) | Created on start, destroyed on stop | Lost every time the AppHost stops | -| `WithLifetime(ContainerLifetime.Persistent)` only | Stays running between AppHost runs | Survives AppHost restarts, but **lost if the container is recreated** (config change, pruning, image update) | -| `WithDataVolume()` only | Created on start, destroyed on stop | Persists in a named volume—**survives container recreation** | -| Both (recommended for databases) | Stays running between AppHost runs | Persists in a named volume—survives container recreation | - -For **databases and other stateful services**, use both APIs together so you get fast startup (the container stays running) _and_ data safety (a volume protects data even if the container is recreated): - - - -```csharp title="AppHost.cs" -var postgres = builder.AddPostgres("postgres") - .WithLifetime(ContainerLifetime.Persistent) - .WithDataVolume(); -``` - - -```typescript title="apphost.ts" -const postgres = await builder.addPostgres("postgres") - .withLifetime(ContainerLifetime.Persistent) - .withDataVolume(); -``` - - - -For **caches or other ephemeral state**, `WithLifetime(ContainerLifetime.Persistent)` alone may be sufficient because losing data on container recreation is acceptable. - - diff --git a/src/frontend/src/content/docs/app-host/resource-lifetimes.mdx b/src/frontend/src/content/docs/app-host/resource-lifetimes.mdx new file mode 100644 index 000000000..3335e6aa7 --- /dev/null +++ b/src/frontend/src/content/docs/app-host/resource-lifetimes.mdx @@ -0,0 +1,452 @@ +--- +title: Configure resource lifetimes in Aspire +description: Learn how session, persistent, resource-scoped, and parent-process lifetimes control Aspire containers, executables, and projects. +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; +import { Image } from 'astro:assets'; +import persistentContainer from '@assets/whats-new/aspire-9/persistent-container.png'; +import persistentContainerDocker from '@assets/whats-new/aspire-9/persistent-container-docker-desktop.png'; + +Aspire resources support a number of different lifetime modes. For example, the default **session lifetime** starts a resource when the AppHost starts and shuts it down when the AppHost exits. A **persistent lifetime** leaves a resource running when the AppHost exits and can reuse the same instance on the next run. + +Resource lifetimes apply to containers, executables, and projects. Persistent executable and project lifetimes are experimental in Aspire 13.4. You can use different lifetimes for resources that take time to initialize, need stable local endpoints, should remain available while you restart or rebuild the AppHost, or need to match another resource's lifetime. + +:::caution[Experimental shared lifetime APIs] +The shared lifetime APIs are experimental and emit diagnostic `ASPIREPERSISTENCE001`. The existing container-specific `WithLifetime(ContainerLifetime.Persistent)` and `WithLifetime(ContainerLifetime.Session)` APIs remain supported for container resources. +::: + +## Lifetime modes + +Use the shared lifetime APIs for new code. They support container, executable, and project resources. + +### Session lifetime + +A session lifetime creates the resource when the AppHost starts and disposes of it when the AppHost stops. This is the default lifetime for resources, so you usually don't need to configure it explicitly. + +Use session lifetime for resources that should only exist while the AppHost is running, such as local test dependencies, temporary containers, or processes that don't need stable state across runs. Session resources also default to proxied endpoints, which are available while the AppHost is running. + +If you previously configured another lifetime and want to return a resource to the default behavior, call `WithSessionLifetime()`. For container resources, `WithLifetime(ContainerLifetime.Session)` is still supported. + +### Persistent lifetime + +A persistent lifetime reuses a previously created resource when possible and doesn't dispose of it when the AppHost stops. Configure this behavior with `WithPersistentLifetime()`, or with the existing container-specific `WithLifetime(ContainerLifetime.Persistent)` API for container resources. + +Use persistent lifetime for resources that are expensive to initialize, need stable local endpoints, or should remain available while you restart or rebuild the AppHost. Common examples include databases, message brokers, emulators, long-running executables, and project resources that should continue running after the AppHost exits. + +:::caution[Configuration changes can recreate persistent resources] +Persistent resources are automatically recreated when the AppHost detects meaningful configuration changes. If the configuration differs, the resource is recreated with the new settings. +::: + +Persistent resources default to proxyless endpoints so a stable local endpoint can remain reachable after the AppHost stops. You can still configure endpoint proxy behavior explicitly. For example, set `isProxied: true` or `IsProxied = true` when you need Aspire's proxy for a specific endpoint, or disable endpoint proxy support when you intentionally want every endpoint on a resource to be proxyless. Persistent executable endpoints must have a concrete `port` or `targetPort`; automatically persisted random executable ports aren't supported. + +:::danger[Persistent container ≠ persistent data] +Persistent container lifetime doesn't guarantee data durability. For details, see [Container lifetime vs. data durability](#container-lifetime-vs-data-durability). +::: + +:::note[Persistent resources and replicas] +Persistent resources don't support replicas because they depend on a single unique resource identifier to be resolved across AppHost runs. + +Persistent resources aren't compatible with Aspire IDE debugging sessions. If you need to debug a persistent executable or project, use your debugger's attach mode if one is available. +::: + +### Parent-process lifetime + +A parent-process lifetime keeps a resource available across AppHost restarts, but scopes cleanup to a parent process. Configure this behavior with `WithParentProcessLifetime(processId)`. + +Use parent-process lifetime for resources that should outlive an individual AppHost run but still be cleaned up when a broader development tool, IDE, or other owning process exits. + +Parent-process lifetime resources share persistent resource behavior across AppHost runs. If Aspire detects meaningful configuration changes on a subsequent run, the resource is recreated with the new settings. + +The parent process ID must be the valid ID of a running process. Aspire records both the process ID and the process identity timestamp so cleanup follows the specific process instance instead of accidentally matching a reused process ID. + +### Resource-scoped lifetime + +A resource-scoped lifetime configures one resource to use another resource's effective lifetime. Configure this behavior with `WithLifetimeOf(resource)`. + +Use resource-scoped lifetime when a companion resource should follow the lifetime choice of another resource. This is useful for sidecars, helper executables, or child resources that should become persistent only when the resource they support is persistent. + +Aspire evaluates the source resource's lifetime when it prepares the application model, so later lifetime changes to the source resource are reflected by the dependent resource. The source and dependent resources must both support lifetime configuration. + +## Configure a persistent container + +For new code, configure a persistent container with `WithPersistentLifetime()`: + + + + +```csharp title="AppHost.cs" +var builder = DistributedApplication.CreateBuilder(args); + +var postgres = builder.AddPostgres("postgres") + .WithPersistentLifetime() + .WithDataVolume(); + +var db = postgres.AddDatabase("inventorydb"); + +builder.AddProject("inventory") + .WithReference(db); + +builder.Build().Run(); +``` + + + + +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const postgres = await builder.addPostgres('postgres'); +await postgres.withPersistentLifetime(); +await postgres.withDataVolume(); + +const db = postgres.addDatabase('inventorydb'); + +const inventory = await builder.addProject( + 'inventory', + './InventoryService/InventoryService.csproj' +); +await inventory.withReference(db); + +await builder.build().run(); +``` + + + + +In the preceding example, the PostgreSQL container persists between AppHost runs, and `WithDataVolume()` stores database data in a named volume that survives container recreation. The `inventory` project references the database as normal. + +## Configure a persistent executable + +Executable resources can also use persistent lifetimes. Persistent executables are useful for local services that have expensive startup, need stable process identity, or should remain reachable while the AppHost restarts. + + + + +```csharp title="AppHost.cs" +var builder = DistributedApplication.CreateBuilder(args); + +var worker = builder.AddExecutable("worker", "node", "../worker", "server.js") + .WithHttpEndpoint(port: 5050, targetPort: 5050) + .WithPersistentLifetime(); + +builder.Build().Run(); +``` + + + + +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const worker = await builder.addExecutable('worker', 'node', '../worker', [ + 'server.js', +]); +await worker.withHttpEndpoint({ port: 5050, targetPort: 5050 }); +await worker.withPersistentLifetime(); + +await builder.build().run(); +``` + + + + +Configure a concrete `port` or `targetPort` for persistent executable endpoints; automatically persisted random executable ports aren't supported. + +## Configure a persistent project + +Project resources can use persistent lifetimes when you want the project process to continue running after the AppHost exits. + + + + +```csharp title="AppHost.cs" +var builder = DistributedApplication.CreateBuilder(args); + +var api = builder.AddProject("api") + .WithPersistentLifetime(); + +builder.Build().Run(); +``` + + + + +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const api = await builder.addProject('api', '../ApiService/ApiService.csproj'); +await api.withPersistentLifetime(); + +await builder.build().run(); +``` + + + + +Persistent project and executable resources are run by Aspire's orchestrator so it can manage their lifecycle consistently. Persistent project and executable resources don't support replicas. + +## Match another resource's lifetime + +Use `WithLifetimeOf` when a companion resource should follow another resource's lifetime. This is useful when a sidecar, helper process, or supporting service should become persistent only when its source resource is persistent. + + + + +```csharp title="AppHost.cs" +var builder = DistributedApplication.CreateBuilder(args); + +var database = builder.AddPostgres("postgres") + .WithPersistentLifetime() + .WithDataVolume(); + +var companion = builder.AddExecutable("companion", "dotnet", "../Companion", "Companion.dll") + .WithLifetimeOf(database); + +builder.Build().Run(); +``` + + + + +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const database = await builder.addPostgres('postgres'); +await database.withPersistentLifetime(); +await database.withDataVolume(); + +const companion = await builder.addExecutable( + 'companion', + 'dotnet', + '../Companion', + ['Companion.dll'] +); +await companion.withLifetimeOf(database); + +await builder.build().run(); +``` + + + + +The dependent resource's lifetime is evaluated when Aspire prepares the application model, so later changes to the source resource's lifetime are reflected by the dependent resource. + +## Scope cleanup to a parent process + +Use `WithParentProcessLifetime` when a resource should survive AppHost restarts but be cleaned up when another process exits. Aspire records the parent process identity instead of retaining a live process handle, so the cleanup scope follows the specific process instance instead of a reused process ID. + + + + +```csharp title="AppHost.cs" +var builder = DistributedApplication.CreateBuilder(args); + +var parentProcessId = int.Parse(builder.Configuration["RESOURCE_PARENT_PROCESS_ID"]!); + +var worker = builder.AddExecutable("scoped-worker", "node", "../worker", "server.js") + .WithParentProcessLifetime(parentProcessId); + +builder.Build().Run(); +``` + + + + +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const parentProcessId = 1234; + +const worker = await builder.addExecutable( + 'scoped-worker', + 'node', + '../worker', + ['server.js'] +); +await worker.withParentProcessLifetime(parentProcessId); + +await builder.build().run(); +``` + + + + +The parent process ID must be greater than zero and identify a running process. + +## Use the container-specific lifetime API + +The older container-specific lifetime API is still supported. Use `WithLifetime(ContainerLifetime.Persistent)` to keep a container running across AppHost restarts, or `WithLifetime(ContainerLifetime.Session)` to explicitly use the default session behavior. + +For new code, prefer the shared `WithPersistentLifetime()` and `WithSessionLifetime()` APIs because they work consistently across containers, executables, and projects. + + + + +```csharp title="AppHost.cs" +var builder = DistributedApplication.CreateBuilder(args); + +var postgres = builder.AddPostgres("postgres") + .WithLifetime(ContainerLifetime.Persistent) + .WithDataVolume(); + +builder.Build().Run(); +``` + + + + +```typescript title="apphost.mts" twoslash +import { ContainerLifetime, createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const postgres = await builder.addPostgres('postgres'); +await postgres.withLifetime(ContainerLifetime.Persistent); +await postgres.withDataVolume(); + +await builder.build().run(); +``` + + + + +## Dashboard visualization + +The Aspire dashboard shows persistent resources with a distinctive pin icon to help you identify them: + +Screenshot of the Aspire dashboard showing a persistent resource with a pin icon. + +After the AppHost stops, persistent containers continue running and can be seen in your container runtime (such as Docker Desktop): + +Screenshot of Docker Desktop showing a persistent RabbitMQ container still running after the AppHost stopped. + +## Container naming and uniqueness + +By default, persistent containers use a naming pattern that combines: + +- The service name you specify in your AppHost. +- A postfix based on a hash of the AppHost project path. + +This naming scheme ensures that persistent containers are unique to each AppHost project, preventing conflicts when multiple Aspire projects use the same service names. + +For example, if you have a service named `"postgres"` in an AppHost project located at `/path/to/MyApp.AppHost`, the container name might be `postgres-abc123def` where `abc123def` is derived from the project path hash. + +### Custom container names + +For advanced scenarios, you can set a custom container name using the `WithContainerName` method: + + + + +```csharp title="AppHost.cs" +var builder = DistributedApplication.CreateBuilder(args); + +var postgres = builder.AddPostgres("postgres") + .WithPersistentLifetime() + .WithContainerName("my-shared-postgres"); + +builder.Build().Run(); +``` + + + + +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const postgres = await builder.addPostgres('postgres'); +await postgres.withPersistentLifetime(); +await postgres.withContainerName('my-shared-postgres'); + +await builder.build().run(); +``` + + + + +When you specify a custom container name, Aspire first checks if a container with that name already exists. If a container with that name exists and was previously created by Aspire, it follows the normal persistent container behavior and can be automatically recreated if the configuration changes. If a container with that name exists but wasn't created by Aspire, it won't be managed or recreated by the AppHost. If no container with the custom name exists, Aspire creates a new one. + +## Executable and project naming and uniqueness + +Persistent executable and project resources are scoped to a specific AppHost instance and uniquely identified by their resource name within that scope. Two executable or project resources with the same name in different AppHosts don't collide with each other; they result in separate process instances. + +## Manual cleanup + +:::caution +Persistent resources aren't automatically removed when you stop the AppHost. To delete them, stop and remove the underlying container or process with the resource's runtime or operating system tools. +::: + +For persistent containers, use Docker CLI commands, Docker Desktop, or your preferred container management tool to stop and remove the container: + +```bash title="Stop and remove a persistent container" +# Stop the container +docker stop my-container-name + +# Remove the container +docker rm my-container-name +``` + +For persistent executable and project resources, stop the running process with your operating system process manager or terminal tools. You can also stop a persistent resource from the Aspire dashboard if the runtime-specific cleanup option isn't straightforward. + +## Container lifetime vs. data durability + +`WithPersistentLifetime()` and `WithDataVolume()` serve different purposes and are often used together. The following table summarizes the behavior of each combination for container resources: + +| Configuration | Container behavior | Data behavior | +| -------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| Neither (default) | Created on start, destroyed on stop | Lost every time the AppHost stops | +| `WithPersistentLifetime()` only | Stays running between AppHost runs | Survives AppHost restarts, but **lost if the container is recreated** (config change, pruning, image update) | +| `WithDataVolume()` only | Created on start, destroyed on stop | Persists in a named volume—**survives container recreation** | +| Both (recommended for databases) | Stays running between AppHost runs | Persists in a named volume—survives container recreation | + +For **databases and other stateful services**, use both APIs together so you get fast startup (the container stays running) _and_ data safety (a volume protects data even if the container is recreated): + + + + +```csharp title="AppHost.cs" +var postgres = builder.AddPostgres("postgres") + .WithPersistentLifetime() + .WithDataVolume(); +``` + + + + +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const postgres = await builder.addPostgres('postgres'); +await postgres.withPersistentLifetime(); +await postgres.withDataVolume(); +``` + + + + +For **caches or other ephemeral state**, `WithPersistentLifetime()` alone may be sufficient because losing data on container recreation is acceptable. + +:::tip +For more details on volumes and bind mounts, see [Persist data using volumes](/fundamentals/persist-data-volumes/). +::: diff --git a/src/frontend/src/content/docs/app-host/typescript-apphost.mdx b/src/frontend/src/content/docs/app-host/typescript-apphost.mdx index 37009fc3b..dc936b3fa 100644 --- a/src/frontend/src/content/docs/app-host/typescript-apphost.mdx +++ b/src/frontend/src/content/docs/app-host/typescript-apphost.mdx @@ -1,24 +1,43 @@ --- title: TypeScript AppHost project structure -description: Learn about the files and configuration that make up a TypeScript AppHost project. +seoTitle: Aspire TypeScript AppHost project structure overview +description: Learn the files and configuration that make up a TypeScript AppHost project — entry point, package manifest, dependencies, and how the AppHost runs Aspire resources. --- import { Aside, FileTree, Steps, Tabs, TabItem } from '@astrojs/starlight/components'; import LearnMore from '@components/LearnMore.astro'; -When you create a TypeScript AppHost with `aspire new` or `aspire init --language typescript`, the CLI scaffolds a project with the following structure: +When you create a TypeScript AppHost with `aspire new`, the CLI scaffolds a project with the following structure: - my-apphost/ - - .modules/ Generated TypeScript SDK (do not edit) - - aspire.ts - - base.ts - - transport.ts - - apphost.ts Your AppHost entry point + - .aspire/modules/ Generated TypeScript SDK (do not edit) + - aspire.mts + - base.mts + - transport.mts + - apphost.mts Your AppHost entry point - aspire.config.json Aspire configuration - package.json - - tsconfig.json + - tsconfig.apphost.json + + + +When you run `aspire init --language typescript` in an existing JavaScript or TypeScript app that already has a root `package.json`, Aspire creates the AppHost in a nested `aspire-apphost/` package. The root `aspire.config.json` points to `aspire-apphost/apphost.mts`, and the root `package.json` gets Aspire delegate scripts so the existing app package keeps its own module and toolchain settings: + + + +- my-existing-app/ + - aspire-apphost/ + - .aspire/modules/ Generated TypeScript SDK (do not edit) + - aspire.mts + - base.mts + - transport.mts + - apphost.mts Your AppHost entry point + - package.json + - tsconfig.apphost.json + - aspire.config.json Aspire configuration + - package.json Existing app package with Aspire delegate scripts @@ -29,7 +48,7 @@ The `aspire.config.json` file is the central configuration for your AppHost. It ```json title="aspire.config.json" { "appHost": { - "path": "apphost.ts", + "path": "apphost.mts", "language": "typescript/nodejs" }, "packages": { @@ -51,20 +70,20 @@ The `aspire.config.json` file is the central configuration for your AppHost. It | Section | Description | |---------|-------------| -| `appHost.path` | Path to your AppHost entry point (`apphost.ts`) | +| `appHost.path` | Path to your AppHost entry point (`apphost.mts`) | | `appHost.language` | Language runtime (`typescript/nodejs`) | | `packages` | Hosting integration packages and their versions. Added automatically by `aspire add`. | | `profiles` | Launch profiles with dashboard URLs and environment variables | -### Adding integrations +### Add and restore integrations -When you run `aspire add`, the CLI adds the package to the `packages` section and regenerates the TypeScript SDK: +Use `aspire add` from the AppHost root to add hosting integrations. The CLI adds the package to the `packages` section, restores AppHost dependencies, and regenerates the TypeScript SDK in `.aspire/modules/`: ```bash title="Add an integration" aspire add redis ``` -This updates `aspire.config.json`: +This updates `aspire.config.json` so the package is restored the next time the AppHost runs: ```json title="aspire.config.json" ins={4} { @@ -75,6 +94,12 @@ This updates `aspire.config.json`: } ``` +Run `aspire restore` when you want to regenerate `.aspire/modules/` without starting the AppHost, such as after switching branches, updating package versions, or preparing a CI job: + +```bash title="Restore a TypeScript AppHost" +aspire restore +``` + ### Project references for local development You can reference a local hosting integration project by using a `.csproj` path instead of a version: @@ -91,15 +116,15 @@ You can reference a local hosting integration project by using a `.csproj` path See [Multi-language integrations](/extensibility/multi-language-integration-authoring/) for details on building hosting integrations that work with TypeScript AppHosts. -## .modules/ directory +## .aspire/modules/ directory -The `.modules/` directory contains the generated TypeScript SDK. It's created and updated automatically by the Aspire CLI — **do not edit these files**. +The `.aspire/modules/` directory under the AppHost root contains the generated TypeScript SDK. It's created and updated automatically by the Aspire CLI — **do not edit these files**. | File | Purpose | |------|---------| -| `aspire.ts` | Generated typed API for all your installed integrations | -| `base.ts` | Base types and handle infrastructure | -| `transport.ts` | JSON-RPC transport layer | +| `aspire.mts` | Generated typed API for all your installed integrations | +| `base.mts` | Base types and handle infrastructure | +| `transport.mts` | JSON-RPC transport layer | The SDK regenerates when: @@ -107,22 +132,22 @@ The SDK regenerates when: - You run `aspire run` or `aspire start` and the package list has changed - You run `aspire restore` to manually regenerate -Your `apphost.ts` imports from this SDK: +Your `apphost.mts` imports from this SDK: -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; ``` -## apphost.ts +## apphost.mts The entry point for your AppHost. This is where you define your application's resources and their relationships: -```typescript title="apphost.ts" twoslash -import { createBuilder } from './.modules/aspire.js'; +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); @@ -136,23 +161,124 @@ const api = await builder await builder.build().run(); ``` +## Legacy `apphost.ts` projects (pre-13.4) + +TypeScript AppHosts scaffolded by Aspire CLI versions earlier than 13.4 use a slightly different layout: the entry point was `apphost.ts` (not `apphost.mts`), and the generated TypeScript SDK lives in `./.modules/` (not `./.aspire/modules/`): + + + +- my-apphost/ + - .modules/ Generated TypeScript SDK (do not edit) + - aspire.ts + - base.ts + - transport.ts + - apphost.ts Your AppHost entry point + - aspire.config.json Aspire configuration + - package.json + - tsconfig.apphost.json + + + +The pre-13.4 entry point imports the SDK with the `.js` extension: + +```typescript title="apphost.ts (pre-13.4)" +import { createBuilder } from './.modules/aspire.js'; +``` + +### Compatibility in 13.4 + +The 13.4 Aspire CLI keeps these projects working without any changes: + +- When the CLI sees an `apphost.ts` file with no `apphost.mts` alongside it — either because `aspire.config.json` sets `appHost.path` to `apphost.ts`, or because a disk scan finds `apphost.ts` and no `apphost.mts` — it switches to the legacy output layout for that project. +- Generated SDK files are written to `./.modules/` instead of `./.aspire/modules/`. +- Generated files are rewritten from `.mts`/`.mjs` to `.ts`/`.js`, including the inter-module import specifiers inside the SDK, so the existing `./.modules/aspire.js` import in your `apphost.ts` continues to resolve. + +`aspire add`, `aspire restore`, `aspire run`, and `aspire start` all continue to work against an existing `apphost.ts` project with no edits required. + + + +### Migrating to the new `apphost.mts` layout + +If you'd like to move an existing project to the new default layout, the migration is mechanical. The steps below assume an AppHost root with `apphost.ts`, `./.modules/`, and the legacy `appHost.path` value: + + + +1. **Rename the entry point file.** + + ```bash + git mv apphost.ts apphost.mts + ``` + +2. **Update the SDK import** in `apphost.mts` to use the new folder and the `.mjs` extension: + + ```typescript title="apphost.mts" del={1} ins={2} + import { createBuilder } from './.modules/aspire.js'; + import { createBuilder } from './.aspire/modules/aspire.mjs'; + ``` + +3. **Update `appHost.path`** in `aspire.config.json`: + + ```json title="aspire.config.json" del={3} ins={4} + { + "appHost": { + "path": "apphost.ts", + "path": "apphost.mts", + "language": "typescript/nodejs" + } + } + ``` + +4. **Update `tsconfig.apphost.json`** so its `include` (and any related glob) entries point at the new file and folder. Replace references to `apphost.ts` with `apphost.mts`, and references to `.modules/` with `.aspire/modules/`. For example: + + ```json title="tsconfig.apphost.json" del={4,5} ins={6,7} + { + "compilerOptions": { /* ... */ }, + "include": [ + "apphost.ts", + ".modules/**/*.ts", + "apphost.mts", + ".aspire/modules/**/*.mts" + ] + } + ``` + +5. **Delete the old generated SDK folder and regenerate.** The CLI now writes the SDK under `./.aspire/modules/`, so the old folder is no longer used: + + ```bash + rm -rf .modules + aspire restore + ``` + + If your `.gitignore` ignores `.modules/`, replace that entry with `.aspire/` (or `.aspire/modules/`) to match the new location. + + + +After migrating, `aspire run` behaves exactly the same as before — the CLI now uses the modern output path because `apphost.mts` is present. + ## Package managers -The Aspire CLI automatically detects which package manager your TypeScript AppHost uses by inspecting lock files and the `packageManager` field in `package.json`. The following package managers are supported: +The Aspire CLI supports the following package managers at the **AppHost root** — the directory that contains your `apphost.mts` and `aspire.config.json`. The CLI selects between them by inspecting package manager signals, including the `packageManager` field in `package.json`, lock files, and package manager configuration in the AppHost root. -| Package manager | Lock file detected | Notes | +| Package manager | Detection signals | Version expectation | |---|---|---| -| npm | `package-lock.json` | Default; no extra setup required | -| pnpm | `pnpm-lock.yaml` | | -| Yarn (v4+) | `yarn.lock` | Must be Yarn 4 or later (Berry) | -| Bun | `bun.lock` / `bun.lockb` | | +| npm | `packageManager`, `package-lock.json`, or no other signal | npm 10 or later; npm is the default | +| pnpm | `packageManager` or `pnpm-lock.yaml` | pnpm 10 or later | +| Yarn | `packageManager`, `yarn.lock`, `.yarnrc.yml`, or `.yarn/` | Yarn 4 or later (Berry) | +| Bun | `packageManager`, `bun.lock`, or `bun.lockb` | Bun 1.2 or later | +| Yarn Classic (v1) | `yarn.lock` with `# yarn lockfile v1` or `packageManager` with `yarn@1.x` | Not supported | + +This policy governs the **AppHost root only**. Apps the AppHost orchestrates — for example, a Node.js service added with `addNodeApp`, a Bun guest app, or a workspace package — can use any package manager their own tooling requires; they are independent of the AppHost-root toolchain. + +Aspire end-to-end tests cover TypeScript AppHosts with representative `packageManager` pins such as `npm@10.0.0`, `pnpm@10.0.0`, `yarn@4.14.1`, and `bun@1.2.0`. These tested versions are representative points within the supported ranges, not the only versions you can use.