Summary
aspire init for a TypeScript AppHost currently scaffolds apphost.ts with top-level await:
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:
async function main(): Promise<void> {
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:
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.
Summary
aspire initfor a TypeScript AppHost currently scaffoldsapphost.tswith top-level await:That works when the AppHost is in an ESM package scope, but it fails in CommonJS/brownfield repos where the nearest
package.jsonomits"type": "module"or explicitly uses CommonJS. In those repos, Node/tsx classify.tsaccording to the package scope, soapphost.tsis treated as CommonJS and top-level await is rejected.We should not fix this by changing the user's root
package.jsonto"type": "module"; that can break existing monorepos and app/tooling assumptions.Observed impact
A React CommonJS turborepo initialized with
aspire initneeded manual changes before the generated TypeScript AppHost would run. Renaming the file to.mtsmade the file ESM, but the Aspire CLI did not recognize.mtsas 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 awaitGenerate an async
main()wrapper instead:Pros:
apphost.tsfilename and detection behavior.Cons:
Option 2: Support an explicitly ESM AppHost file (
apphost.mts)Generate or support
apphost.mtsfor TypeScript AppHosts that use top-level await.Pros:
Cons:
.mtsis scaffolded/detected/type-checked/run correctly.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.tsat the repository root, create a subfolder such asaspire-apphost/containing:{ "type": "module" }Then keep
aspire-apphost/apphost.tswith top-level await and pointaspire.config.jsonat that relative path.Pros:
apphost.tsand top-level await.Cons / things to validate:
.modulesgeneration location and imports fromapphost.ts.aspire.config.jsonpathing, detection, restore, run, and watch behavior when the AppHost is below the repo root.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.