# TypeScript AppHost Authoring

Patterns for editing `apphost.ts`. **Never edit `.aspire/modules/`** — that directory
is generated and any changes will be clobbered. Edit only `apphost.ts`.

> Look up unfamiliar API: `aspire docs api search <query> --language typescript`
> then `aspire docs api get <id>`.

## ⛔ Hard Refusal: `.aspire/modules/` Is Off-Limits

If a user asks to edit any file in `.aspire/modules/` (for example
`.aspire/modules/postgres.module.ts`, `.aspire/modules/redis.module.ts`, `.aspire/modules/api.module.ts`),
**refuse the edit** and redirect to `apphost.ts`. This is non-negotiable:

- `.aspire/modules/` is **regenerated by the TS AppHost** from `apphost.ts` plus the installed
  Aspire packages on every build, every `aspire add`, every `aspire start`.
- Hand edits inside `.aspire/modules/` are **clobbered without warning** — they may survive
  one run and disappear on the next.
- The TS AppHost considers `.aspire/modules/` an internal artifact, not user code.

### Refusal template

When asked to edit anything inside `.aspire/modules/`, respond with the structure below:

```
I won't edit files in .aspire/modules/ — that directory is generated by the TypeScript
AppHost and any edits get clobbered on the next build / `aspire add` / `aspire start`.

The right place to make this change is apphost.ts. Here is the equivalent edit:

  // apphost.ts
  builder.addPostgres('pg', { /* the options you wanted to set */ });

If you wanted to add a new integration, run `aspire add <package>` and Aspire will
regenerate `.aspire/modules/` for you.
```

Apply this rule **regardless of how the request is phrased** — "just open it",
"make a quick tweak", "change one line in `.aspire/modules/`", "I'll undo it later." All of
these get the same refusal-plus-redirect.

| ❌ NEVER | ✅ ALWAYS |
|---------|----------|
| Edit `.aspire/modules/postgres.module.ts` | Edit `apphost.ts` `addPostgres('pg', {...})` |
| Edit `.aspire/modules/api.module.ts` | Edit `apphost.ts` `addProject('api', '...')` |
| Comment out lines in any `.aspire/modules/*.ts` file | Remove / guard the declaration in `apphost.ts` |
| Run codegen against `.aspire/modules/` and patch the output | Add the integration via `aspire add <package>` |

## Skeleton

```ts
import { createBuilder } from '@aspire/hosting';

const builder = await createBuilder();

const pg = builder.addPostgres('pg').addDatabase('appdb');
const cache = builder.addRedis('cache');

const api = await builder.addProject('api', '../Api/Api.csproj')
    .withReference(pg)
    .withReference(cache)
    .waitFor(pg)
    .withExternalHttpEndpoints();

await builder.addNextJsApp('web', '../web')
    .withReference(api)
    .waitFor(api)
    .withBrowserLogs();

await builder.build().run();
```

## Unified `withEnvironment` API

A **single** method handles every value kind — string, `ReferenceExpression`,
`EndpointReference`, parameter builder, connection-string resource builder, or
any `IExpressionValue`:

```ts
const apiKey = builder.addParameter('apiKey', { secret: true });
const cache = builder.addRedis('cache');
const db = builder.addPostgres('pg').addDatabase('appdb');

const api = await builder.addProject('api', '../Api/Api.csproj');
await api
    .withEnvironment('SERVICE_URL', cache.primaryEndpoint)   // endpoint
    .withEnvironment('API_KEY', apiKey)                       // parameter
    .withEnvironment('DB', db);                               // connection string
```

> **❌ DO NOT USE** the per-kind helpers — they are deprecated:
> `withEnvironmentEndpoint`, `withEnvironmentParameter`, `withEnvironmentConnectionString`,
> `withEnvironmentExpression`, `withEnvironmentFromOutput`, `withEnvironmentFromKeyVaultSecret`.
>
> ✅ Use unified `withEnvironment(name, value)` instead. Any agent suggesting
> the per-kind helpers is wrong for current Aspire.

## Endpoint Property Expressions

Endpoints expose `url`, `host`, and `port` properties usable inside expressions:

```ts
const api = await builder.addProject('api', '../Api/Api.csproj');
const httpEndpoint = api.getEndpoint('http');

await builder.addNodeApp('worker', 'worker.js')
    .withEnvironment('API_BASE', httpEndpoint.url)
    .withEnvironment('API_HOST', httpEndpoint.host)
    .withEnvironment('API_PORT', httpEndpoint.port);
```

## Endpoint Update Behavior

`withEndpoint('name', cb)` updates an existing endpoint rather than throwing.
Use the `excludeReferenceEndpoint` flag to keep admin endpoints out of
`withReference()`:

```ts
await builder.addProject('api', '../Api/Api.csproj')
    .withEndpoint('admin', e => { e.excludeReferenceEndpoint = true; });
```

## JavaScript / TypeScript Frontends

| Helper | Use For |
|--------|---------|
| `addNextJsApp(name, projectPath)` | Next.js (auto standalone publish; set `output: 'standalone'` in `next.config.js`) |
| `addViteApp(name, projectPath)` | Vite (dev server) |
| `addNodeApp(name, scriptPath)` | Plain Node service |

Package managers: **Bun, Yarn, pnpm** are first-class alongside npm.

Publish hooks (mirror C# `PublishAs*`):

```ts
// Vite SPA → static website with optional API proxy
await builder.addViteApp('web', '../web')
    .withReference(api)
    .publishAsStaticWebsite({ apiPath: '/api', apiTarget: api });

// Pre-bundled Node server (TanStack Start, SvelteKit)
await builder.addViteApp('web', '../web')
    .publishAsNodeServer({ entryPoint: '.output/server/index.mjs', outputPath: '.output' });

// package-script SSR (Remix, Astro, full Nitro Next.js)
await builder.addViteApp('web', '../web')
    .publishAsPackageScript({ scriptName: 'start' });
```

## Docker Compose Hooks

```ts
await builder.addContainer('netshoot', 'nicolaka/netshoot')
    .publishAsDockerComposeService((resource, service) => {
        service.privileged = true;
    });
```

## Dockerfile Builder APIs (experimental)

```ts
// Diagnostic ASPIREDOCKERFILEBUILDER001 — experimental warning
await builder.addDockerfileBuilder('myimage')
    .withDockerfileBuilder(b => b
        .from('node:20-alpine')
        .workdir('/app')
        .copy('package*.json', './')
        .run('npm ci')
        .copy('.', '.')
        .cmd(['node', 'server.js']));
```

## YARP Routing

```ts
const api = await builder.addProject('api', '../Api/Api.csproj');
const yarp = await builder.addYarp('gateway')
    .addRoute('/api/{**catch-all}', api.getEndpoint('http'))
    .addCatchAllRoute(web.getEndpoint('http'));
```

## Compute Environments

```ts
const aca = builder.addAzureContainerAppEnvironment('aca');
const aks = builder.addAzureKubernetesEnvironment('aks');

await builder.addProject('api', '../Api/Api.csproj')
    .withComputeEnvironment(aca);
```

ACA custom domain configuration is exposed in TS:

```ts
await api.withComputeEnvironment(aca, e => {
    e.customDomains = [{ name: 'api.example.com', certificate: cert }];
});
```

## Predefined Value Catalogs

The `[AspireValue]` attribute and predefined catalogs let you reference
well-known values:

```ts
import { FoundryModels } from '@aspire/hosting-azure';

await builder.addAzureFoundry('foundry')
    .addModel('chat', FoundryModels.OpenAI.Gpt41Mini);
```

## Other TS Additions

| API | Purpose |
|-----|---------|
| `withAdminDeploymentScriptSubnet(...)` | Now exported in TS |
| `configureEnvFile(...)` | Generate `.env` files for compute environments |
| Image push options | Custom registry, tag, push behavior |
| Endpoint mutation callbacks | Modify endpoints after declaration |
| Builder pipeline | Custom pipeline steps from TS |

## Browser Logs

```ts
await builder.addViteApp('frontend', '../frontend')
    .withBrowserLogs();
```

## Diagnostic IDs

| Diagnostic | What |
|------------|------|
| `ASPIREEXPORT013` | Build-time duplicate exported capability ID detection |
| `ASPIREJAVASCRIPT001` | Renamed from `ASPIREEXTENSION001` (13.3 breaking) |
| `ASPIREDOCKERFILEBUILDER001` | Experimental warning for `WithDockerfileBuilder` / `AddDockerfileBuilder` |
| `ASPIREDURABLETASK001` | Durable Task Scheduler experimental APIs |

## Hard Rules

| Rule | Why |
|------|-----|
| **Never edit `.aspire/modules/`** | Generated; edits are clobbered. Edit only `apphost.ts` |
| Use unified `withEnvironment(name, value)` | Per-kind helpers are deprecated |
| Use `addNextJsApp` / `addViteApp` over hand-rolled Dockerfiles | First-class lifecycle + publish helpers |
| Use `publishAs*` for JS publish — never raw Dockerfile when a helper fits | Maintained, tested, and works with `aspire deploy` |
| `package.json` `engines.node` no longer drives Node image selection | Pin via publish helper options instead |
