[Java starter] Make the AppHost a project
- Dominant language
- C#
- Stars
- 6.3k
- Forks
- 991
- Avg merge
- 2d 15h
- Merged PRs (30d)
- 196
Description
## Description
The `aspire-java-starter` template should scaffold its root AppHost as a standalone Gradle project instead of maintaining a separate Eclipse/JDT project model.
The nested Spring Boot API is already a Gradle project and imports reliably. The root AppHost currently depends on generated `.project`, `.classpath`, `.settings`, and VS Code source-path metadata to make `AppHost.java` and `.aspire/modules` visible to the Java language server. That model is not importing reliably in a normal VS Code Insiders workspace.
@ellahathaway, tagging you since you reviewed the project-model work in #19635.
## Background
#19635 added Eclipse/JDT metadata while intentionally leaving the AppHost on the CLI's default `javac` toolchain. In the current 13.6 daily starter, the nested `api` project imports and has IntelliSense, but the root AppHost is assigned to JDT's fallback project and reports:
```text
AppHost.java is a non-project file, only syntax errors are reported Java(16)
```
JDT's internal workspace contains `api` and `jdt.ls-java-project`, but not the named root project from `.project`. The fallback project links `AppHost.java` without the generated `.aspire/modules` source root, so completion, semantic diagnostics, and definition navigation do not work in the AppHost.
Rather than adding more importer-specific metadata or extension recovery logic, I think the starter should use Aspire's existing Gradle AppHost project model.
## Repro
1. Generate `aspire-java-starter` using the current 13.6 daily CLI.
2. Open the generated directory in VS Code Insiders with the recommended Aspire and Java extensions installed.
3. Wait for Java project import to finish.
4. Open `AppHost.java` and try completion or go-to-definition on `DistributedApplication`.
5. Open a Java source file under `api/src/main/java` and compare the language support.
## Proposed layout
Use a standalone Gradle build at the workspace root and keep `api` as an independent nested Gradle build:
```text
AppHost.java
build.gradle
settings.gradle
gradlew
gradlew.bat
gradle/wrapper/...
.aspire/modules/...
api/
build.gradle
settings.gradle
gradlew
gradle/wrapper/...
frontend/...
```
The root `settings.gradle` should not include `api`. Keeping the builds independent avoids changing the generated Spring Boot project or coupling its lifecycle to the AppHost build. The tradeoff is a second wrapper, which seems preferable to a custom IDE-only project model.
Keep `AppHost.java` at the workspace root so `aspire.config.json` and the AppHost's existing relative resource paths do not change.
## Environment
- macOS 26.6.2
- VS Code Insiders 1.136.0-insider
- Aspire CLI `13.6.0-preview.1.26451.9+039a7c58f11a01521e49882e26d0d6593b87d9cf`
- Aspire VS Code extension 1.22.0
- Red Hat Java `1.56.2026082811`
- Java Debugger `0.59.2026072407`
- OpenJDK 25 from Homebrew
## Gradle project contract
The root `build.gradle` can follow the existing `playground/JavaSpringBoot/JavaSpringBoot.AppHost.Java` project:
- apply the Java plugin;
- target Java 25 through both the toolchain and source/target compatibility so Buildship gives JDT the correct compliance level;
- use Maven Central for AppHost dependencies;
- model `.` and `.aspire/modules` as separate Java source roots;
- exclude `.aspire/**` from the `.` root so the source roots do not overlap;
- include only Java files from the root and disable the resources source set so workspace and Gradle files are not copied into build output.
The existing CLI already detects a root `build.gradle` as `JavaAppHostToolchain.Gradle`. Its Gradle path uses the wrapper and generated init script to stage `runtimeClasspath` dependencies under `build/aspire-deps`, then compiles the AppHost consistently with the other Java AppHost toolchains using `javac`. This proposal should use that existing contract; it does not need to replace the CLI's compiler invocation with `gradle compileJava`.
## Template changes
Add to `src/Aspire.Cli/Templating/Templates/java-starter`:
- `build.gradle`
- `settings.gradle`
- `gradlew`
- `gradlew.bat`
- `gradle/wrapper/gradle-wrapper.jar`
- `gradle/wrapper/gradle-wrapper.properties`
Remove the IDE-specific root project model:
- `.project`
- `.classpath`
- `.settings/org.eclipse.jdt.core.prefs`
- `.vscode/settings.json`, once the Gradle model is the source of truth for both source roots
Keep `AppHost.java` and its `aspire.config.json` path unchanged.
## Test changes
Update `tests/Aspire.Cli.Tests/Templating/JavaStarterScaffoldTests.cs` to assert that a generated starter:
- contains the root Gradle build and wrapper;
- resolves as `JavaAppHostToolchain.Gradle` rather than `JavaAppHostToolchain.Javac`;
- exposes `AppHost.java` and `.aspire/modules` through the Gradle source set;
- no longer emits the Eclipse/JDT project files.
Keep and strengthen `extension/src/test-e2e/javaStarterProjectModel.e2e.test.ts` to prove the user path:
- both the root AppHost Gradle project and nested `api` Gradle project import;
- completion works in `AppHost.java` before starting the application;
- `DistributedApplication` navigates into `.aspire/modules`;
- no Java(16) non-project diagnostic is reported;
- `aspire run` still compiles and starts the generated AppHost and API.
The resolver already has focused Gradle AppHost coverage, but it should gain a regression case matching this exact standalone-root-plus-nested-build topology if that shape is not currently covered.
## Acceptance criteria
- A newly generated `aspire-java-starter` has a standalone Gradle project for its root AppHost.
- `AppHost.java` remains at the workspace root and `.aspire/modules` is a separate modeled source root.
- The nested `api` remains an independent Gradle build and retains working IntelliSense and launch behavior.
- Opening the starter produces no Java(16) non-project diagnostic for `AppHost.java`.
- Completion, semantic diagnostics, and definition navigation work in the AppHost before `aspire run`.
- `aspire run` uses the existing Gradle AppHost toolchain path, stages declared dependencies, compiles the AppHost, and starts the full starter successfully.
- The template no longer relies on checked-in Eclipse/JDT metadata or VS Code source-path settings for the root AppHost model.
Contributor guide
Research direction
Start with src/Aspire.Cli/Templating/Templates/java-starter and compare it with playground/JavaSpringBoot/JavaSpringBoot.AppHost.Java, then inspect the existing Gradle toolchain resolver. Update tests/Aspire.Cli.Tests/Templating/JavaStarterScaffoldTests.cs and extension/src/test-e2e/javaStarterProjectModel.e2e.test.ts for the standalone root build, source roots, imports, navigation, diagnostics, and aspire run behavior. Done means the generated starter uses the Gradle AppHost toolchain without Eclipse/JDT or VS Code root-project metadata.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, java, typescript
- Domain
- build-system, cli, developer-experience, testing
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100