Skip to content
170 changes: 170 additions & 0 deletions tests/Aspire.Cli.EndToEnd.Tests/TypeScriptPublishTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ namespace Aspire.Cli.EndToEnd.Tests;
/// </summary>
public sealed class TypeScriptPublishTests(ITestOutputHelper output)
{
private static readonly string s_jsPublishFixturesDir = Path.Combine(
CliE2ETestHelpers.GetRepoRoot(),
"tests", "Aspire.Cli.EndToEnd.Tests", "Fixtures", "JsPublish");

[Fact]
public async Task PublishWithDockerComposeServiceCallbackSucceeds()
{
Expand Down Expand Up @@ -100,6 +104,104 @@ public async Task PublishWithDockerComposeServiceCallbackSucceeds()
await pendingRun;
}

[Fact]
[CaptureWorkspaceOnFailure]
public async Task PublishJavaScriptPatternsGeneratesExpectedDockerComposeArtifacts()
{
var repoRoot = CliE2ETestHelpers.GetRepoRoot();
var strategy = CliInstallStrategy.Detect(output.WriteLine);
using var workspace = TemporaryWorkspace.Create(output);
var localChannel = CliE2ETestHelpers.PrepareLocalChannel(repoRoot, strategy,
["Aspire.Hosting.CodeGeneration.TypeScript.", "Aspire.Hosting.JavaScript.", "Aspire.Hosting.Docker."]);

using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.Polyglot, 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.TypeAsync("aspire init --language typescript --non-interactive");
await auto.EnterAsync();
await auto.WaitUntilTextAsync("Created apphost.ts", timeout: TimeSpan.FromMinutes(2));
await auto.WaitForSuccessPromptAsync(counter);

if (localChannel is not null)
{
CliE2ETestHelpers.WriteLocalChannelSettings(workspace.WorkspaceRoot.FullName, localChannel.SdkVersion);
}

await auto.TypeAsync("aspire add Aspire.Hosting.JavaScript");
await auto.EnterAsync();
await auto.WaitForAspireAddSuccessAsync(counter, TimeSpan.FromMinutes(2));

await auto.TypeAsync("aspire add Aspire.Hosting.Docker");
await auto.EnterAsync();
await auto.WaitForAspireAddSuccessAsync(counter, TimeSpan.FromMinutes(2));

CopyJavaScriptPublishFixtures(workspace);
WriteJavaScriptPublishAppHost(workspace);

await auto.TypeAsync("unset ASPIRE_PLAYGROUND");
await auto.EnterAsync();
await auto.WaitForSuccessPromptAsync(counter);

await auto.TypeAsync("aspire publish -o artifacts --non-interactive");
await auto.EnterAsync();
await auto.WaitForSuccessPromptFailFastAsync(counter, timeout: TimeSpan.FromMinutes(5));

var artifactsPath = Path.Combine(workspace.WorkspaceRoot.FullName, "artifacts");
var composeContent = await File.ReadAllTextAsync(Path.Combine(artifactsPath, "docker-compose.yaml"));

Assert.Contains("staticsite:", composeContent);
Assert.Contains("nodeserver:", composeContent);
Assert.Contains("npmscript:", composeContent);
Assert.Contains("nextjs:", composeContent);

AssertDockerfileContains(
artifactsPath,
"staticsite",
"COPY --from=build /app/dist /app/wwwroot",
"ENTRYPOINT [\"dotnet\",\"/app/yarp.dll\"]");

AssertDockerfileContains(
artifactsPath,
"nodeserver",
"COPY --from=build /app/build /app/build",
"ENV NODE_ENV=production",
"USER node",
Comment thread
sebastienros marked this conversation as resolved.
"ENTRYPOINT [\"node\",\"build/server.js\"]");

AssertDockerfileContains(
artifactsPath,
"npmscript",
"FROM ",
" AS prod-deps",
"RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev",
"COPY --from=build /app /app",
"COPY --from=prod-deps /app/node_modules ./node_modules",
"ENV NODE_ENV=production",
"ENTRYPOINT [\"sh\",\"-c\",\"exec npm run start -- --port $PORT\"]");

AssertDockerfileContains(
artifactsPath,
"nextjs",
"COPY --from=build --chown=node:node /app/public ./public",
"COPY --from=build --chown=node:node /app/.next/standalone ./",
"COPY --from=build --chown=node:node /app/.next/static ./.next/static",
"USER node",
"ENTRYPOINT [\"node\",\"server.js\"]");

await auto.TypeAsync("exit");
await auto.EnterAsync();

await pendingRun;
}

[Fact]
[CaptureWorkspaceOnFailure]
public async Task PublishWithoutOutputPathUsesAppHostDirectoryDefault()
Expand Down Expand Up @@ -276,4 +378,72 @@ await compose.configureEnvFile(async (envVars) => {

await pendingRun;
}

private static void WriteJavaScriptPublishAppHost(TemporaryWorkspace workspace)
{
var appHostPath = Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.ts");
File.WriteAllText(appHostPath, """
import { createBuilder } from './.modules/aspire.js';

const builder = await createBuilder();

const compose = await builder.addDockerComposeEnvironment('compose');
await compose.withDashboard({ enabled: false });

const api = await builder.addNodeApp('api', './api', 'server.js')
.withHttpEndpoint({ port: 3001, env: 'PORT' });

await builder.addViteApp('staticsite', './staticsite')
.withHttpEndpoint({ name: 'http', targetPort: 5000 })
.publishAsStaticWebsite({ apiPath: '/api', apiTarget: api });

await builder.addViteApp('nodeserver', './nodeserver')
.publishAsNodeServer('build/server.js', { outputPath: 'build' });

await builder.addViteApp('npmscript', './npmscript')
.publishAsNpmScript({ startScriptName: 'start', runScriptArguments: '-- --port $PORT' });

await builder.addNextJsApp('nextjs', './nextjs');

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

private static void CopyJavaScriptPublishFixtures(TemporaryWorkspace workspace)
{
foreach (var fixtureName in new[] { "api", "staticsite", "nodeserver", "npmscript", "nextjs" })
{
CopyDirectory(
Path.Combine(s_jsPublishFixturesDir, fixtureName),
Path.Combine(workspace.WorkspaceRoot.FullName, fixtureName));
}
}

private static void CopyDirectory(string source, string destination)
{
Directory.CreateDirectory(destination);

foreach (var file in Directory.GetFiles(source))
{
File.Copy(file, Path.Combine(destination, Path.GetFileName(file)));
}

foreach (var dir in Directory.GetDirectories(source))
{
CopyDirectory(dir, Path.Combine(destination, Path.GetFileName(dir)));
}
}

private static void AssertDockerfileContains(string artifactsPath, string resourceName, params string[] expectedFragments)
{
var dockerfilePath = Path.Combine(artifactsPath, $"{resourceName}.Dockerfile");
Assert.True(File.Exists(dockerfilePath), $"Expected Dockerfile for resource '{resourceName}' at {dockerfilePath}");

var content = File.ReadAllText(dockerfilePath);

foreach (var expectedFragment in expectedFragments)
{
Assert.Contains(expectedFragment, content);
}
}
Comment thread
sebastienros marked this conversation as resolved.
}
Loading