Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "api",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "node server.js",
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "nextjs",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "nodeserver",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "node server.js",
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "npmscript",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "node server.js",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "staticsite",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "npx serve dist",
Expand Down
268 changes: 249 additions & 19 deletions tests/Aspire.Cli.EndToEnd.Tests/JavaScriptPublishTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using Aspire.Cli.EndToEnd.Tests.Helpers;
using Aspire.Cli.Tests.Utils;
using Aspire.TestUtilities;
using Hex1b.Automation;
using Xunit;

Expand All @@ -22,12 +21,14 @@ public sealed class JavaScriptPublishTests(ITestOutputHelper output)

[Fact]
[CaptureWorkspaceOnFailure]
[QuarantinedTest("https://github.com/microsoft/aspire/issues/16188")]
public async Task AllPublishMethodsBuildDockerImages()
{
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."]);
var channelArgument = localChannel is not null ? " --channel local" : string.Empty;
Comment thread
sebastienros marked this conversation as resolved.
Outdated

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

Expand All @@ -39,7 +40,7 @@ public async Task AllPublishMethodsBuildDockerImages()
await auto.InstallAspireCliAsync(strategy, counter);

// Create TS AppHost and add packages
await auto.TypeAsync("aspire init");
await auto.TypeAsync($"aspire init{channelArgument}");
await auto.EnterAsync();
await auto.WaitUntilTextAsync("Which language would you like to use?", timeout: TimeSpan.FromSeconds(30));
await auto.DownAsync();
Expand All @@ -48,13 +49,18 @@ public async Task AllPublishMethodsBuildDockerImages()
await auto.WaitUntilTextAsync("Created apphost.ts", timeout: TimeSpan.FromMinutes(2));
await auto.DeclineAgentInitPromptAsync(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.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(180));
await auto.WaitForAspireAddCompletionAsync(counter, TimeSpan.FromSeconds(180));

await auto.TypeAsync("aspire add Aspire.Hosting.Docker");
await auto.EnterAsync();
await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(180));
await auto.WaitForAspireAddCompletionAsync(counter, TimeSpan.FromSeconds(180));

// Copy checked-in fixture apps and write the apphost
CopyFixtures(workspace);
Expand Down Expand Up @@ -86,6 +92,90 @@ public async Task AllPublishMethodsBuildDockerImages()
await pendingRun;
}

[Fact]
[CaptureWorkspaceOnFailure]
public async Task JavaScriptHostingApisRunFromTypeScriptAppHost()
{
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."]);
var channelArgument = localChannel is not null ? " --channel local" : string.Empty;

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

var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
var counter = new SequenceCounter();
var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
var testBodyFailed = false;

try
{
await auto.PrepareDockerEnvironmentAsync(counter, workspace);
await auto.InstallAspireCliAsync(strategy, counter);

await auto.RunCommandFailFastAsync($"aspire init --language typescript --non-interactive{channelArgument}", counter, TimeSpan.FromMinutes(2));

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.WaitForAspireAddCompletionAsync(counter, TimeSpan.FromMinutes(2));

WriteRuntimeFixtures(workspace);
WriteRuntimeAppHost(workspace);
WriteRuntimeVerificationScript(workspace);

await auto.RunCommandFailFastAsync("unset ASPIRE_PLAYGROUND", counter);

await auto.RunCommandFailFastAsync("aspire run > aspire-run.log 2>&1 & echo $! > aspire-run.pid", counter);
await auto.RunCommandFailFastAsync("bash verify-runtime.sh", counter, TimeSpan.FromMinutes(2));
}
catch
{
testBodyFailed = true;
throw;
}
finally
{
try
{
await auto.RunCommandAsync("if [ -f aspire-run.pid ]; then kill \"$(cat aspire-run.pid)\" 2>/dev/null || true; wait \"$(cat aspire-run.pid)\" 2>/dev/null || true; fi", counter, TimeSpan.FromMinutes(1));
}
catch
{
// Best effort. A failure before aspire run writes its PID leaves no process to stop.
}

try
{
await auto.CaptureAspireDiagnosticsAsync(counter, workspace);
}
catch
{
// Best effort diagnostics capture.
}

try
{
await auto.TypeAsync("exit");
await auto.EnterAsync();
await pendingRun;
}
catch
{
if (!testBodyFailed)
{
throw;
}
}
}
}

private static void WriteAppHost(TemporaryWorkspace workspace)
{
var appHostPath = Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.ts");
Expand All @@ -95,29 +185,169 @@ private static void WriteAppHost(TemporaryWorkspace workspace)
const builder = await createBuilder();
await builder.addDockerComposeEnvironment('compose');

const api = await builder.addNodeApp('api', './api', 'server.js')
.withHttpEndpoint({ port: 3001, env: 'PORT' })
.withExternalHttpEndpoints();
const api = await builder.addNodeApp('api', './api', 'server.js');
await api.withHttpEndpoint({ port: 3001, env: 'PORT' });
Comment thread
sebastienros marked this conversation as resolved.
Outdated
await api.withExternalHttpEndpoints();

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

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

const npmscript = await builder.addViteApp('npmscript', './npmscript');
await npmscript.publishAsNpmScript({ startScriptName: 'start' });
await npmscript.withExternalHttpEndpoints();

const nextjs = await builder.addNextJsApp('nextjs', './nextjs');
await nextjs.withExternalHttpEndpoints();

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

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

const builder = await createBuilder();

await builder.addViteApp('staticsite', './staticsite')
.publishAsStaticWebsite({ apiPath: '/api', apiTarget: api })
.withExternalHttpEndpoints();
const nodeNpm = await builder.addNodeApp('node-npm', './node-npm', 'server.js');
await nodeNpm.withNpm({ install: false });
await nodeNpm.withRunScript('start');
await nodeNpm.withHttpEndpoint({ name: 'http', env: 'PORT' });

await builder.addViteApp('nodeserver', './nodeserver')
.publishAsNodeServer('build/server.js', { outputPath: 'build' })
.withExternalHttpEndpoints();
const javaScriptPnpm = await builder.addJavaScriptApp('javascript-pnpm', './javascript-pnpm', { runScriptName: 'dev' });
await javaScriptPnpm.withPnpm({ install: false });

await builder.addViteApp('npmscript', './npmscript')
.publishAsNpmScript({ startScriptName: 'start' })
.withExternalHttpEndpoints();
const viteYarn = await builder.addViteApp('vite-yarn', './vite-yarn', { runScriptName: 'dev' });
await viteYarn.withYarn({ install: false });

await builder.addNextJsApp('nextjs', './nextjs')
.withExternalHttpEndpoints();
const nextBun = await builder.addNextJsApp('next-bun', './next-bun', { runScriptName: 'dev' });
await nextBun.disableBuildValidation();
await nextBun.withBun({ install: false });

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

private static void WriteRuntimeFixtures(TemporaryWorkspace workspace)
{
WriteRuntimeApp(workspace, "node-npm", "start");
WriteRuntimeApp(workspace, "javascript-pnpm", "dev");
WriteRuntimeApp(workspace, "vite-yarn", "dev", packageManager: "yarn@4.14.1");
WriteRuntimeApp(workspace, "next-bun", "dev");
}

private static void WriteRuntimeApp(TemporaryWorkspace workspace, string appName, string scriptName, string? packageManager = null)
{
var appDir = Path.Combine(workspace.WorkspaceRoot.FullName, appName);
Directory.CreateDirectory(appDir);

var packageManagerProperty = packageManager is not null ? $"""
"packageManager": "{packageManager}",
""" : string.Empty;

File.WriteAllText(Path.Combine(appDir, "package.json"), $$"""
{
"name": "{{appName}}",
"private": true,
{{packageManagerProperty}}
"scripts": {
"{{scriptName}}": "node server.js"
}
}
""");

if (packageManager?.StartsWith("yarn@", StringComparison.OrdinalIgnoreCase) == true)
{
// Yarn 2+ walks up to the AppHost package.json unless the nested app has its own
// project boundary. The lock file keeps this fixture isolated from the TypeScript
// AppHost project while still avoiding an install step.
File.WriteAllText(Path.Combine(appDir, "yarn.lock"), """
# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!

__metadata:
version: 8
cacheKey: 10c0

""");
}

File.WriteAllText(Path.Combine(appDir, "server.js"), $$"""
const http = require('http');
const fs = require('fs');

const portArgumentIndex = process.argv.findIndex(arg => arg === '--port' || arg === '-p');
const fallbackPorts = {
'node-npm': 3001,
'javascript-pnpm': 3002,
'vite-yarn': 3003,
'next-bun': 3004,
};
const port = process.env.PORT || (portArgumentIndex >= 0 ? process.argv[portArgumentIndex + 1] : undefined) || fallbackPorts['{{appName}}'];

http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ app: '{{appName}}', ok: true }));
}).listen(port, '0.0.0.0', () => {
fs.writeFileSync('ready-port', port.toString());
console.log('{{appName}} listening on ' + port);
});
""");
}

private static void WriteRuntimeVerificationScript(TemporaryWorkspace workspace)
{
File.WriteAllText(Path.Combine(workspace.WorkspaceRoot.FullName, "verify-runtime.sh"), $$"""
#!/usr/bin/env bash
set -euo pipefail

check_endpoint() {
local name="$1"
local port_file="${name}/ready-port"

for i in $(seq 1 30); do
if [ ! -f "${port_file}" ]; then
sleep 1
continue
fi

local port
port="$(cat "${port_file}")"
if curl -sf --max-time 5 "http://localhost:${port}/" | grep -q "\"app\":\"${name}\""; then
echo "${name}_OK"
return 0
fi

sleep 1
done

echo "${name}_FAIL"
echo "===== aspire-run.log ====="
cat aspire-run.log || true
echo "===== end aspire-run.log ====="
echo "===== runtime fixture files ====="
find . -maxdepth 3 \( -name package.json -o -name yarn.lock -o -name ready-port \) -print -exec sh -c 'echo "--- $1"; cat "$1"' _ {} \; || true
echo "===== end runtime fixture files ====="
return 1
}

check_endpoint "node-npm"
check_endpoint "javascript-pnpm"
check_endpoint "vite-yarn"
check_endpoint "next-bun"

echo "RUNTIME_ALL_OK"
""");
}

private static void CopyFixtures(TemporaryWorkspace workspace)
{
// Copy root-level files (e.g. verify.sh)
Expand Down
Loading
Loading