Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 31 additions & 24 deletions src/Aspire.Cli/Templating/Templates/py-starter/apphost.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
import { createBuilder } from './.modules/aspire.js';

const builder = await createBuilder();
async function main() {
const builder = await createBuilder();

// {{#redis}}
// Add a Redis cache for the app to use.
const cache = await builder
.addRedis("cache");
// {{#redis}}
// Add a Redis cache for the app to use.
const cache = await builder
.addRedis("cache");

// {{/redis}}
// Run the Python FastAPI app and expose its HTTP endpoint externally.
const app = await builder
.addUvicornApp("app", "./app", "main:app")
.withUv()
.withExternalHttpEndpoints()
// {{#redis}}
.withReference(cache)
.waitFor(cache)
// {{/redis}}
.withHttpHealthCheck("/health");
// {{/redis}}
// Run the Python FastAPI app and expose its HTTP endpoint externally.
const app = await builder
.addUvicornApp("app", "./app", "main:app")
.withUv()
.withExternalHttpEndpoints()
// {{#redis}}
.withReference(cache)
.waitFor(cache)
// {{/redis}}
.withHttpHealthCheck("/health");

// Run the Vite frontend after the API and inject the API URL for local proxying.
const frontend = await builder
.addViteApp("frontend", "./frontend")
.withReference(app)
.waitFor(app);
// Run the Vite frontend after the API and inject the API URL for local proxying.
const frontend = await builder
.addViteApp("frontend", "./frontend")
.withReference(app)
.waitFor(app);

// Bundle the frontend build output into the API container for publish/deploy.
await app.publishWithContainerFiles(frontend, "./static");
// Bundle the frontend build output into the API container for publish/deploy.
await app.publishWithContainerFiles(frontend, "./static");

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

main().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
35 changes: 21 additions & 14 deletions src/Aspire.Cli/Templating/Templates/ts-starter/apphost.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import { createBuilder } from './.modules/aspire.js';

const builder = await createBuilder();
async function main() {
const builder = await createBuilder();

// Run the Express API and expose its HTTP endpoint externally.
const app = await builder
.addNodeApp("app", "./api", "src/index.ts")
.withHttpEndpoint({ env: "PORT" })
.withExternalHttpEndpoints();
// Run the Express API and expose its HTTP endpoint externally.
const app = await builder
.addNodeApp("app", "./api", "src/index.ts")
.withHttpEndpoint({ env: "PORT" })
.withExternalHttpEndpoints();

// Run the Vite frontend after the API and inject the API URL for local proxying.
const frontend = await builder
.addViteApp("frontend", "./frontend")
.withReference(app)
.waitFor(app);
// Run the Vite frontend after the API and inject the API URL for local proxying.
const frontend = await builder
.addViteApp("frontend", "./frontend")
.withReference(app)
.waitFor(app);

// Bundle the frontend build output into the API container for publish/deploy.
await app.publishWithContainerFiles(frontend, "./static");
// Bundle the frontend build output into the API container for publish/deploy.
await app.publishWithContainerFiles(frontend, "./static");

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

main().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,20 @@ public Dictionary<string, string> Scaffold(ScaffoldRequest request)

import { createBuilder } from './.modules/aspire.js';

const builder = await createBuilder();
async function main() {
const builder = await createBuilder();

// Add your resources here, for example:
// const redis = await builder.addContainer("cache", "redis:latest");
// const postgres = await builder.addPostgres("db");
// Add your resources here, for example:
// const redis = await builder.addContainer("cache", "redis:latest");
// const postgres = await builder.addPostgres("db");

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

main().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
""";

files[PackageJsonFileName] = CreatePackageJson(request);
Expand Down
98 changes: 95 additions & 3 deletions tests/Aspire.Cli.EndToEnd.Tests/TypeScriptPolyglotTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@ public async Task InitTypeScriptAppHost_AugmentsExistingViteRepoAtRoot()
var packages = packagesNode!.AsObject();
Assert.NotNull(packages["Aspire.Hosting.JavaScript"]);

await auto.TypeAsync("npm run aspire:build");
await auto.EnterAsync();
await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(2));

// Modify apphost.ts to add the Vite app before running
var appHostPath = Path.Combine(projectRoot, "apphost.ts");
var newContent = """
Expand All @@ -223,11 +227,18 @@ public async Task InitTypeScriptAppHost_AugmentsExistingViteRepoAtRoot()

import { createBuilder } from './.modules/aspire.js';

const builder = await createBuilder();
async function main() {
const builder = await createBuilder();

await builder.addViteApp("brownfield", ".");
await builder.addViteApp("brownfield", ".");

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

main().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
""";

File.WriteAllText(appHostPath, newContent);
Expand All @@ -254,4 +265,85 @@ await auto.WaitUntilAsync(s =>

await pendingRun;
}

[Fact]
[CaptureWorkspaceOnFailure]
public async Task InitTypeScriptAppHost_AugmentsExistingCommonJsRepoAtRoot()
{
var repoRoot = CliE2ETestHelpers.GetRepoRoot();
var strategy = CliInstallStrategy.Detect(output.WriteLine);
var workspace = TemporaryWorkspace.Create(output);
var localChannel = CliE2ETestHelpers.PrepareLocalChannel(repoRoot, strategy,
["Aspire.Hosting.CodeGeneration.TypeScript."]);
var channelArgument = localChannel is not null ? " --channel local" : string.Empty;

using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.DotNet, mountDockerSocket: true, workspace: workspace);

var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);

var counter = new SequenceCounter();
var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));

await auto.PrepareDockerEnvironmentAsync(counter, workspace);

await auto.InstallAspireCliAsync(strategy, counter);
await auto.EnablePolyglotSupportAsync(counter);

await auto.TypeAsync("mkdir commonjs && cd commonjs");
await auto.EnterAsync();
await auto.WaitForSuccessPromptAsync(counter);

File.WriteAllText(Path.Combine(workspace.WorkspaceRoot.FullName, "commonjs", "package.json"), """
{
"private": true,
"scripts": {},
"devDependencies": {
"typescript": "5.9.3"
}
}
""");

var projectRoot = Path.Combine(workspace.WorkspaceRoot.FullName, "commonjs");
if (localChannel is not null)
{
CliE2ETestHelpers.WriteLocalChannelSettings(projectRoot, localChannel.SdkVersion);
}

await auto.TypeAsync($"aspire init --language typescript --non-interactive{channelArgument}");
await auto.EnterAsync();
await auto.WaitUntilTextAsync("Created apphost.ts", timeout: TimeSpan.FromMinutes(2));
await auto.DeclineAgentInitPromptAsync(counter);

var packageJson = JsonNode.Parse(File.ReadAllText(Path.Combine(projectRoot, "package.json")))!.AsObject();
Assert.Null(packageJson["type"]);

var appHostContent = File.ReadAllText(Path.Combine(projectRoot, "apphost.ts"));
Assert.Contains("async function main()", appHostContent);
Assert.Contains("main().catch((error: unknown) =>", appHostContent);
Assert.Contains("process.exitCode = 1;", appHostContent);

await auto.TypeAsync("npm run aspire:build");
await auto.EnterAsync();
await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(2));

await auto.TypeAsync("aspire run");
await auto.EnterAsync();
await auto.WaitUntilAsync(s =>
{
if (s.ContainsText("Top-level await is currently not supported with the \"cjs\" output format") ||
s.ContainsText("TS1309"))
{
throw new InvalidOperationException("TypeScript AppHost used top-level await in a CommonJS project.");
}

return s.ContainsText("Press CTRL+C to stop the AppHost and exit.");
}, timeout: TimeSpan.FromMinutes(3), description: "Press CTRL+C message (CommonJS aspire run started)");

await auto.Ctrl().KeyAsync(Hex1bKey.C);
await auto.WaitForSuccessPromptAsync(counter);
await auto.TypeAsync("exit");
await auto.EnterAsync();

await pendingRun;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ public void Scaffold_CreatesAppHostSpecificScriptsAndTsConfig_ForNewProject()
Assert.DoesNotContain("\\u003E", files["package.json"]);

Assert.Contains("eslint.config.mjs", files.Keys);
Assert.Contains("async function main()", files["apphost.ts"]);
Assert.Contains("main().catch((error: unknown) =>", files["apphost.ts"]);
Assert.Contains("process.exitCode = 1;", files["apphost.ts"]);
Assert.DoesNotContain("ignoreVoid: true", files["eslint.config.mjs"]);

var tsConfig = ParseJson(files["tsconfig.apphost.json"]);
Assert.Equal("./dist/apphost", tsConfig["compilerOptions"]?["outDir"]?.GetValue<string>());
Expand Down Expand Up @@ -109,6 +113,9 @@ public void Scaffold_BrownfieldOutput_ContainsOnlyAspireEntries()
Assert.False(scripts.ContainsKey("dev"));
Assert.False(scripts.ContainsKey("build"));
Assert.False(scripts.ContainsKey("preview"));
Assert.Contains("async function main()", files["apphost.ts"]);
Assert.Contains("main().catch((error: unknown) =>", files["apphost.ts"]);
Assert.Contains("process.exitCode = 1;", files["apphost.ts"]);

// Scaffold should only contain Aspire-desired dependencies (at Aspire's versions)
Assert.Equal("^8.2.0", dependencies["vscode-jsonrpc"]?.GetValue<string>());
Expand Down
Loading