microsoft / microsoft/aspire

Decide TypeScript AppHost top-level await behavior for CommonJS package roots

Open
#16,892 0 comments 0 reactions 0 assignees View on GitHub
needs-area-label
Dominant language
C#
Stars
6.3k
Forks
991
Avg merge
2d 15h
Merged PRs (30d)
196

Description

## Summary

`aspire init` for a TypeScript AppHost currently scaffolds `apphost.ts` with top-level await:

```ts
const builder = await createBuilder();
await builder.build().run();
```

That works when the AppHost is in an ESM package scope, but it fails in CommonJS/brownfield repos where the nearest `package.json` omits `"type": "module"` or explicitly uses CommonJS. In those repos, Node/tsx classify `.ts` according to the package scope, so `apphost.ts` is treated as CommonJS and top-level await is rejected.

We should not fix this by changing the user's root `package.json` to `"type": "module"`; that can break existing monorepos and app/tooling assumptions.

## Observed impact

A React CommonJS turborepo initialized with `aspire init` needed manual changes before the generated TypeScript AppHost would run. Renaming the file to `.mts` made the file ESM, but the Aspire CLI did not recognize `.mts` as an AppHost, so the workaround had to fall back to rewriting the template to avoid top-level await.

## Options

### Option 1: Keep `apphost.ts`, remove top-level await

Generate an async `main()` wrapper instead:

```ts
async function main(): Promise {
const builder = await createBuilder();
await builder.build().run();
}

main().catch((err: unknown) => {
console.error(err);
process.exit(1);
});
```

Pros:
- Works in CommonJS and ESM package scopes.
- Keeps current `apphost.ts` filename and detection behavior.
- Lowest CLI/runtime compatibility risk.

Cons:
- More verbose generated template.
- Gives up the cleaner top-level-await style.

### Option 2: Support an explicitly ESM AppHost file (`apphost.mts`)

Generate or support `apphost.mts` for TypeScript AppHosts that use top-level await.

Pros:
- Preserves the clean top-level-await template.
- ESM semantics are explicit and independent of the root package type.

Cons:
- Requires TypeScript language support changes so `.mts` is scaffolded/detected/type-checked/run correctly.
- Existing CLI flows currently look for `apphost.ts`; detection, `aspire.config.json`, templates, and tests need to be updated.

### Option 3: Generate the TypeScript AppHost in a subfolder with a local ESM package boundary

Instead of placing `apphost.ts` at the repository root, create a subfolder such as `aspire-apphost/` containing:

```json
{
"type": "module"
}
```

Then keep `aspire-apphost/apphost.ts` with top-level await and point `aspire.config.json` at that relative path.

Pros:
- Preserves `apphost.ts` and top-level await.
- Avoids changing the monorepo/root package type.
- Gives Aspire-owned files a clearer boundary from the user's existing app/package config.

Cons / things to validate:
- Package manager/workspace behavior for npm, pnpm, yarn, and bun.
- Where dependencies should be installed and whether this creates a nested lockfile/package boundary users dislike.
- `.modules` generation location and imports from `apphost.ts`.
- `aspire.config.json` pathing, detection, restore, run, and watch behavior when the AppHost is below the repo root.
- How aspireify/skills should discover the user's apps from a subfolder AppHost while preserving relative paths to services.

## Recommendation

Discuss the desired product shape before changing the scaffold. If compatibility is the top priority, Option 1 is safest. If preserving top-level await is important, Option 2 or Option 3 should be implemented deliberately and tested across package managers and brownfield monorepos.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.