Decide TypeScript AppHost top-level await behavior for CommonJS package roots
- 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
Assessment
This issue has not been assessed yet.