diff --git a/src/Aspire.Cli/Projects/GuestRuntime.cs b/src/Aspire.Cli/Projects/GuestRuntime.cs index 6a8a3fe8adf..e8a0d601c18 100644 --- a/src/Aspire.Cli/Projects/GuestRuntime.cs +++ b/src/Aspire.Cli/Projects/GuestRuntime.cs @@ -136,10 +136,21 @@ public GuestRuntime(RuntimeSpec spec, ILogger logger, FileLoggerProvider? fileLo IGuestProcessLauncher launcher, CancellationToken cancellationToken) { - var commandSpec = watchMode && _spec.WatchExecute is not null - ? _spec.WatchExecute + var useWatchCommand = watchMode && _spec.WatchExecute is not null; + var commandSpec = useWatchCommand + ? _spec.WatchExecute! : _spec.Execute; + await EnsureMigrationFilesExistAsync(directory, cancellationToken); + if (!useWatchCommand) + { + var preExecuteResult = await RunPreExecuteCommandsAsync(appHostFile, directory, environmentVariables, launcher, cancellationToken); + if (preExecuteResult.ExitCode != 0) + { + return preExecuteResult; + } + } + return await ExecuteCommandAsync(commandSpec, appHostFile, directory, environmentVariables, null, launcher, cancellationToken); } @@ -163,9 +174,45 @@ public GuestRuntime(RuntimeSpec spec, ILogger logger, FileLoggerProvider? fileLo { var commandSpec = _spec.PublishExecute ?? _spec.Execute; + await EnsureMigrationFilesExistAsync(directory, cancellationToken); + var preExecuteResult = await RunPreExecuteCommandsAsync(appHostFile, directory, environmentVariables, launcher, cancellationToken); + if (preExecuteResult.ExitCode != 0) + { + return preExecuteResult; + } + return await ExecuteCommandAsync(commandSpec, appHostFile, directory, environmentVariables, publishArgs, launcher, cancellationToken); } + private async Task<(int ExitCode, OutputCollector? Output)> RunPreExecuteCommandsAsync( + FileInfo appHostFile, + DirectoryInfo directory, + IDictionary environmentVariables, + IGuestProcessLauncher launcher, + CancellationToken cancellationToken) + { + if (_spec.PreExecute is null or { Length: 0 }) + { + return (0, new OutputCollector()); + } + + var preExecuteLauncher = launcher is ExtensionGuestLauncher ? CreateDefaultLauncher() : launcher; + foreach (var commandSpec in _spec.PreExecute) + { + var args = ReplacePlaceholders(commandSpec.Args, appHostFile, directory, null); + var mergedEnvironment = MergeEnvironmentVariables(environmentVariables, commandSpec); + + _logger.LogDebug("Launching pre-execution command: {Command} {Args}", commandSpec.Command, string.Join(" ", args)); + var (exitCode, output) = await preExecuteLauncher.LaunchAsync(commandSpec.Command, args, directory, mergedEnvironment, cancellationToken); + if (exitCode != 0) + { + return (exitCode, output ?? new OutputCollector()); + } + } + + return (0, new OutputCollector()); + } + private async Task<(int ExitCode, OutputCollector? Output)> ExecuteCommandAsync( CommandSpec commandSpec, FileInfo appHostFile, @@ -177,8 +224,16 @@ public GuestRuntime(RuntimeSpec spec, ILogger logger, FileLoggerProvider? fileLo { var args = ReplacePlaceholders(commandSpec.Args, appHostFile, directory, additionalArgs); - await EnsureMigrationFilesExistAsync(directory, cancellationToken); + var mergedEnvironment = MergeEnvironmentVariables(environmentVariables, commandSpec); + + _logger.LogDebug("Launching: {Command} {Args}", commandSpec.Command, string.Join(" ", args)); + return await launcher.LaunchAsync(commandSpec.Command, args, directory, mergedEnvironment, cancellationToken); + } + private static Dictionary MergeEnvironmentVariables( + IDictionary environmentVariables, + CommandSpec commandSpec) + { var mergedEnvironment = new Dictionary(environmentVariables); if (commandSpec.EnvironmentVariables is not null) { @@ -188,8 +243,7 @@ public GuestRuntime(RuntimeSpec spec, ILogger logger, FileLoggerProvider? fileLo } } - _logger.LogDebug("Launching: {Command} {Args}", commandSpec.Command, string.Join(" ", args)); - return await launcher.LaunchAsync(commandSpec.Command, args, directory, mergedEnvironment, cancellationToken); + return mergedEnvironment; } /// diff --git a/src/Aspire.Cli/Projects/TypeScriptAppHostToolchainResolver.cs b/src/Aspire.Cli/Projects/TypeScriptAppHostToolchainResolver.cs index 778d26a44dc..c89cc32d1b5 100644 --- a/src/Aspire.Cli/Projects/TypeScriptAppHostToolchainResolver.cs +++ b/src/Aspire.Cli/Projects/TypeScriptAppHostToolchainResolver.cs @@ -144,6 +144,7 @@ public static RuntimeSpec ApplyToRuntimeSpec(RuntimeSpec baseRuntimeSpec, TypeSc DetectionPatterns = baseRuntimeSpec.DetectionPatterns, Initialize = baseRuntimeSpec.Initialize, InstallDependencies = CreateInstallCommand(toolchain), + PreExecute = CreatePreExecuteCommands(toolchain, tsConfigFileName), Execute = CreateExecuteCommand(toolchain, tsConfigFileName), WatchExecute = CreateWatchCommand(toolchain, tsConfigFileName), PublishExecute = baseRuntimeSpec.PublishExecute, @@ -161,6 +162,32 @@ private static CommandSpec CreateInstallCommand(TypeScriptAppHostToolchain toolc }; } + private static CommandSpec[] CreatePreExecuteCommands(TypeScriptAppHostToolchain toolchain, string tsConfigFileName) + { + return + [ + toolchain switch + { + TypeScriptAppHostToolchain.Bun => new CommandSpec + { + Command = "bun", + Args = ["run", "tsc", "--noEmit", "-p", tsConfigFileName] + }, + TypeScriptAppHostToolchain.Yarn => new CommandSpec + { + Command = "yarn", + Args = ["run", "tsc", "--noEmit", "-p", tsConfigFileName] + }, + TypeScriptAppHostToolchain.Pnpm => new CommandSpec + { + Command = "pnpm", + Args = ["exec", "tsc", "--noEmit", "-p", tsConfigFileName] + }, + _ => throw new ArgumentOutOfRangeException(nameof(toolchain), toolchain, null) + } + ]; + } + private static CommandSpec CreateExecuteCommand(TypeScriptAppHostToolchain toolchain, string tsConfigFileName) { return toolchain switch @@ -170,11 +197,11 @@ private static CommandSpec CreateExecuteCommand(TypeScriptAppHostToolchain toolc Command = "bun", Args = ["run", "{appHostFile}"] }, - TypeScriptAppHostToolchain.Yarn => new CommandSpec - { - Command = "yarn", - Args = ["exec", "tsx", "--tsconfig", tsConfigFileName, "{appHostFile}"] - }, + TypeScriptAppHostToolchain.Yarn => new CommandSpec + { + Command = "yarn", + Args = ["run", "tsx", "--tsconfig", tsConfigFileName, "{appHostFile}"] + }, TypeScriptAppHostToolchain.Pnpm => new CommandSpec { Command = "pnpm", @@ -191,7 +218,17 @@ private static CommandSpec CreateWatchCommand(TypeScriptAppHostToolchain toolcha TypeScriptAppHostToolchain.Bun => new CommandSpec { Command = "bun", - Args = ["--watch", "run", "{appHostFile}"] + Args = + [ + "run", + "nodemon", + "--signal", "SIGTERM", + "--watch", ".", + "--ext", "ts", + "--ignore", "node_modules/", + "--ignore", ".modules/", + "--exec", $"bun run tsc --noEmit -p {tsConfigFileName} && bun run \"{{appHostFile}}\"" + ] }, TypeScriptAppHostToolchain.Yarn => new CommandSpec { @@ -205,7 +242,7 @@ private static CommandSpec CreateWatchCommand(TypeScriptAppHostToolchain toolcha "--ext", "ts", "--ignore", "node_modules/", "--ignore", ".modules/", - "--exec", $"yarn exec tsx --tsconfig {tsConfigFileName} {{appHostFile}}" + "--exec", $"yarn run tsc --noEmit -p {tsConfigFileName} && yarn run tsx --tsconfig {tsConfigFileName} \"{{appHostFile}}\"" ] }, TypeScriptAppHostToolchain.Pnpm => new CommandSpec @@ -220,7 +257,7 @@ private static CommandSpec CreateWatchCommand(TypeScriptAppHostToolchain toolcha "--ext", "ts", "--ignore", "node_modules/", "--ignore", ".modules/", - "--exec", $"pnpm exec tsx --tsconfig {tsConfigFileName} {{appHostFile}}" + "--exec", $"pnpm exec tsc --noEmit -p {tsConfigFileName} && pnpm exec tsx --tsconfig {tsConfigFileName} \"{{appHostFile}}\"" ] }, _ => throw new ArgumentOutOfRangeException(nameof(toolchain), toolchain, null) diff --git a/src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptLanguageSupport.cs b/src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptLanguageSupport.cs index 994f1ec0b5c..9a1c28c34f1 100644 --- a/src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptLanguageSupport.cs +++ b/src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptLanguageSupport.cs @@ -266,6 +266,14 @@ public RuntimeSpec GetRuntimeSpec() Command = "npm", Args = ["install"] }, + PreExecute = + [ + new CommandSpec + { + Command = "npx", + Args = ["--no-install", "tsc", "--noEmit", "-p", AppHostTsConfigFileName] + } + ], Execute = new CommandSpec { Command = "npx", @@ -282,7 +290,7 @@ public RuntimeSpec GetRuntimeSpec() "--ext", "ts", "--ignore", "node_modules/", "--ignore", ".modules/", - "--exec", $"npx --no-install tsx --tsconfig {AppHostTsConfigFileName} {{appHostFile}}" + "--exec", $"npx --no-install tsc --noEmit -p {AppHostTsConfigFileName} && npx --no-install tsx --tsconfig {AppHostTsConfigFileName} \"{{appHostFile}}\"" ] }, MigrationFiles = new Dictionary diff --git a/src/Aspire.TypeSystem/RuntimeSpec.cs b/src/Aspire.TypeSystem/RuntimeSpec.cs index 14e0ad52e40..b0e589bab1b 100644 --- a/src/Aspire.TypeSystem/RuntimeSpec.cs +++ b/src/Aspire.TypeSystem/RuntimeSpec.cs @@ -39,6 +39,12 @@ public sealed class RuntimeSpec /// public CommandSpec? InstallDependencies { get; init; } + /// + /// Gets the commands to run before executing or publishing the AppHost. Null if no pre-execution validation is needed. + /// Watch-mode validation should be part of when needed. + /// + public CommandSpec[]? PreExecute { get; init; } + /// /// Gets the command to execute the AppHost for run. /// diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployTypeScriptTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployTypeScriptTests.cs index b7912b4284c..c1c80ead038 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployTypeScriptTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployTypeScriptTests.cs @@ -96,9 +96,11 @@ public async Task DeployTypeScriptAppToKubernetes() // Add Kubernetes environment with Helm deployment const k8sEnv = await builder.addKubernetesEnvironment("env"); -await k8sEnv.withHelm(async (helm) => { - await helm.withNamespace(k8sNamespace); - await helm.withChartVersion(chartVersion); +await k8sEnv.withHelm({ + configure: async (helm) => { + await helm.withNamespace(k8sNamespace); + await helm.withChartVersion(chartVersion); + }, }); await builder.build().run(); diff --git a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptPublishTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptPublishTests.cs index f43255979bf..35e8bbbc4a8 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptPublishTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptPublishTests.cs @@ -240,7 +240,7 @@ public async Task PublishWithConfigureEnvFileUpdatesEnvOutput() const builder = await createBuilder(); const compose = await builder.addDockerComposeEnvironment("compose"); - await compose.withDashboard(false); + await compose.withDashboard({ enabled: false }); const container = await builder.addContainer("my-container", "nginx:alpine"); await container.withBindMount("/host/path/data", "/container/data"); diff --git a/tests/Aspire.Cli.Tests/Projects/GuestRuntimeTests.cs b/tests/Aspire.Cli.Tests/Projects/GuestRuntimeTests.cs index 61db2c1f2d2..0cfc8d4f544 100644 --- a/tests/Aspire.Cli.Tests/Projects/GuestRuntimeTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/GuestRuntimeTests.cs @@ -28,7 +28,8 @@ private static RuntimeSpec CreateTestSpec( CommandSpec? execute = null, CommandSpec? watchExecute = null, CommandSpec? publishExecute = null, - CommandSpec? installDependencies = null) + CommandSpec? installDependencies = null, + CommandSpec[]? preExecute = null) { return new RuntimeSpec { @@ -43,7 +44,8 @@ private static RuntimeSpec CreateTestSpec( }, WatchExecute = watchExecute, PublishExecute = publishExecute, - InstallDependencies = installDependencies + InstallDependencies = installDependencies, + PreExecute = preExecute }; } @@ -111,6 +113,72 @@ public async Task RunAsync_WatchMode_UsesWatchExecuteSpec() Assert.Contains("--watch", launcher.LastArgs); } + [Fact] + public async Task RunAsync_WatchModeWithWatchExecute_SkipsPreExecute() + { + var spec = CreateTestSpec( + execute: new CommandSpec { Command = "run-cmd", Args = ["{appHostFile}"] }, + watchExecute: new CommandSpec { Command = "watch-cmd", Args = ["--watch", "{appHostFile}"] }, + preExecute: + [ + new CommandSpec { Command = "typecheck-cmd", Args = ["--noEmit"] } + ]); + var runtime = CreateRuntime(spec); + var launcher = new RecordingLauncher(); + var appHostFile = new FileInfo("/tmp/apphost.ts"); + var directory = new DirectoryInfo("/tmp"); + + var (exitCode, _) = await runtime.RunAsync(appHostFile, directory, new Dictionary(), watchMode: true, launcher, CancellationToken.None); + + Assert.Equal(0, exitCode); + var call = Assert.Single(launcher.Calls); + Assert.Equal("watch-cmd", call.Command); + } + + [Fact] + public async Task RunAsync_RunsPreExecuteBeforeExecute() + { + var spec = CreateTestSpec( + execute: new CommandSpec { Command = "run-cmd", Args = ["{appHostFile}"] }, + preExecute: + [ + new CommandSpec { Command = "typecheck-cmd", Args = ["--project", "{appHostDir}"] } + ]); + var runtime = CreateRuntime(spec); + var launcher = new RecordingLauncher(); + var appHostFile = new FileInfo("/tmp/apphost.ts"); + var directory = new DirectoryInfo("/tmp"); + + await runtime.RunAsync(appHostFile, directory, new Dictionary(), watchMode: false, launcher, CancellationToken.None); + + Assert.Equal(2, launcher.Calls.Count); + Assert.Equal("typecheck-cmd", launcher.Calls[0].Command); + Assert.Equal(["--project", directory.FullName], launcher.Calls[0].Args); + Assert.Equal("run-cmd", launcher.Calls[1].Command); + } + + [Fact] + public async Task RunAsync_WhenPreExecuteFails_DoesNotExecute() + { + var spec = CreateTestSpec( + execute: new CommandSpec { Command = "run-cmd", Args = ["{appHostFile}"] }, + preExecute: + [ + new CommandSpec { Command = "typecheck-cmd", Args = ["--noEmit"] } + ]); + var runtime = CreateRuntime(spec); + var launcher = new RecordingLauncher(); + launcher.ExitCodes.Enqueue(2); + var appHostFile = new FileInfo("/tmp/apphost.ts"); + var directory = new DirectoryInfo("/tmp"); + + var (exitCode, _) = await runtime.RunAsync(appHostFile, directory, new Dictionary(), watchMode: false, launcher, CancellationToken.None); + + Assert.Equal(2, exitCode); + var call = Assert.Single(launcher.Calls); + Assert.Equal("typecheck-cmd", call.Command); + } + [Fact] public async Task RunAsync_WatchModeWithoutWatchSpec_FallsBackToExecute() { @@ -143,6 +211,28 @@ public async Task PublishAsync_UsesPublishExecuteSpec() Assert.Contains(launcher.LastArgs, a => a.Contains("--output") && a.Contains("/out")); } + [Fact] + public async Task PublishAsync_RunsPreExecuteBeforePublishExecute() + { + var spec = CreateTestSpec( + execute: new CommandSpec { Command = "run-cmd", Args = ["{appHostFile}"] }, + publishExecute: new CommandSpec { Command = "publish-cmd", Args = ["{appHostFile}", "{args}"] }, + preExecute: + [ + new CommandSpec { Command = "typecheck-cmd", Args = ["--project", "{appHostDir}"] } + ]); + var runtime = CreateRuntime(spec); + var launcher = new RecordingLauncher(); + var appHostFile = new FileInfo("/tmp/apphost.ts"); + var directory = new DirectoryInfo("/tmp"); + + await runtime.PublishAsync(appHostFile, directory, new Dictionary(), ["--output", "/out"], launcher, CancellationToken.None); + + Assert.Equal(2, launcher.Calls.Count); + Assert.Equal("typecheck-cmd", launcher.Calls[0].Command); + Assert.Equal("publish-cmd", launcher.Calls[1].Command); + } + [Fact] public async Task PublishAsync_WithoutPublishSpec_FallsBackToExecute() { @@ -527,6 +617,8 @@ public async Task RunAsync_NoMigrationFiles_ExecutesNormally() private sealed class RecordingLauncher : IGuestProcessLauncher { + public List<(string Command, string[] Args)> Calls { get; } = []; + public Queue ExitCodes { get; } = []; public string LastCommand { get; private set; } = string.Empty; public string[] LastArgs { get; private set; } = []; public DirectoryInfo? LastWorkingDirectory { get; private set; } @@ -539,11 +631,13 @@ private sealed class RecordingLauncher : IGuestProcessLauncher IDictionary environmentVariables, CancellationToken cancellationToken) { + Calls.Add((command, args)); LastCommand = command; LastArgs = args; LastWorkingDirectory = workingDirectory; LastEnvironmentVariables = new Dictionary(environmentVariables); - return Task.FromResult<(int, OutputCollector?)>((0, new OutputCollector())); + var exitCode = ExitCodes.Count > 0 ? ExitCodes.Dequeue() : 0; + return Task.FromResult<(int, OutputCollector?)>((exitCode, new OutputCollector())); } } } diff --git a/tests/Aspire.Cli.Tests/Projects/TypeScriptAppHostToolchainResolverTests.cs b/tests/Aspire.Cli.Tests/Projects/TypeScriptAppHostToolchainResolverTests.cs index 684ee975006..a307a27d5b4 100644 --- a/tests/Aspire.Cli.Tests/Projects/TypeScriptAppHostToolchainResolverTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/TypeScriptAppHostToolchainResolverTests.cs @@ -179,11 +179,25 @@ public void ApplyToRuntimeSpec_WhenBunSelected_UsesBunCommandsAndPreservesExtens Assert.NotNull(runtimeSpec.InstallDependencies); Assert.Equal("bun", runtimeSpec.InstallDependencies?.Command); Assert.Equal(["install"], runtimeSpec.InstallDependencies!.Args); + var preExecute = Assert.Single(runtimeSpec.PreExecute!); + Assert.Equal("bun", preExecute.Command); + Assert.Equal(["run", "tsc", "--noEmit", "-p", "tsconfig.apphost.json"], preExecute.Args); Assert.Equal("bun", runtimeSpec.Execute.Command); Assert.Equal(["run", "{appHostFile}"], runtimeSpec.Execute.Args); Assert.NotNull(runtimeSpec.WatchExecute); Assert.Equal("bun", runtimeSpec.WatchExecute?.Command); - Assert.Equal(["--watch", "run", "{appHostFile}"], runtimeSpec.WatchExecute!.Args); + Assert.Equal( + [ + "run", + "nodemon", + "--signal", "SIGTERM", + "--watch", ".", + "--ext", "ts", + "--ignore", "node_modules/", + "--ignore", ".modules/", + "--exec", "bun run tsc --noEmit -p tsconfig.apphost.json && bun run \"{appHostFile}\"" + ], + runtimeSpec.WatchExecute!.Args); Assert.Equal("node", runtimeSpec.ExtensionLaunchCapability); } @@ -194,10 +208,29 @@ public void ApplyToRuntimeSpec_WhenYarnSelected_UsesYarnExecCommands() var runtimeSpec = TypeScriptAppHostToolchainResolver.ApplyToRuntimeSpec(baseRuntimeSpec, TypeScriptAppHostToolchain.Yarn); + var preExecute = Assert.Single(runtimeSpec.PreExecute!); + Assert.Equal("yarn", preExecute.Command); + Assert.Equal(["run", "tsc", "--noEmit", "-p", "tsconfig.apphost.json"], preExecute.Args); Assert.Equal("yarn", runtimeSpec.Execute.Command); - Assert.Equal(["exec", "tsx", "--tsconfig", "tsconfig.apphost.json", "{appHostFile}"], runtimeSpec.Execute.Args); + Assert.Equal(["run", "tsx", "--tsconfig", "tsconfig.apphost.json", "{appHostFile}"], runtimeSpec.Execute.Args); Assert.Equal("yarn", runtimeSpec.WatchExecute?.Command); - Assert.Contains("yarn exec tsx --tsconfig tsconfig.apphost.json {appHostFile}", runtimeSpec.WatchExecute?.Args ?? []); + Assert.Contains("yarn run tsc --noEmit -p tsconfig.apphost.json && yarn run tsx --tsconfig tsconfig.apphost.json \"{appHostFile}\"", runtimeSpec.WatchExecute?.Args ?? []); + } + + [Fact] + public void ApplyToRuntimeSpec_WhenPnpmSelected_UsesPnpmTypeCheckCommands() + { + var baseRuntimeSpec = CreateBaseRuntimeSpec(); + + var runtimeSpec = TypeScriptAppHostToolchainResolver.ApplyToRuntimeSpec(baseRuntimeSpec, TypeScriptAppHostToolchain.Pnpm); + + var preExecute = Assert.Single(runtimeSpec.PreExecute!); + Assert.Equal("pnpm", preExecute.Command); + Assert.Equal(["exec", "tsc", "--noEmit", "-p", "tsconfig.apphost.json"], preExecute.Args); + Assert.Equal("pnpm", runtimeSpec.Execute.Command); + Assert.Equal(["exec", "tsx", "--tsconfig", "tsconfig.apphost.json", "{appHostFile}"], runtimeSpec.Execute.Args); + Assert.Equal("pnpm", runtimeSpec.WatchExecute?.Command); + Assert.Contains("pnpm exec tsc --noEmit -p tsconfig.apphost.json && pnpm exec tsx --tsconfig tsconfig.apphost.json \"{appHostFile}\"", runtimeSpec.WatchExecute?.Args ?? []); } private static RuntimeSpec CreateBaseRuntimeSpec() @@ -213,6 +246,14 @@ private static RuntimeSpec CreateBaseRuntimeSpec() Command = "npm", Args = ["install"] }, + PreExecute = + [ + new CommandSpec + { + Command = "npx", + Args = ["--no-install", "tsc", "--noEmit", "-p", "tsconfig.apphost.json"] + } + ], Execute = new CommandSpec { Command = "npx", diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/TypeScriptLanguageSupportTests.cs b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/TypeScriptLanguageSupportTests.cs index fca7ec67176..303357bd30d 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/TypeScriptLanguageSupportTests.cs +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/TypeScriptLanguageSupportTests.cs @@ -231,10 +231,13 @@ public void Scaffold_GeneratesProfilePortsOutsideWindowsEphemeralRange(int? port public void GetRuntimeSpec_UsesAppHostSpecificTsConfig() { var runtimeSpec = _languageSupport.GetRuntimeSpec(); + var preExecute = Assert.Single(runtimeSpec.PreExecute!); var watchExecute = Assert.IsType(runtimeSpec.WatchExecute); + Assert.Equal("npx", preExecute.Command); + Assert.Equal(new[] { "--no-install", "tsc", "--noEmit", "-p", "tsconfig.apphost.json" }, preExecute.Args); Assert.Equal(new[] { "--no-install", "tsx", "--tsconfig", "tsconfig.apphost.json", "{appHostFile}" }, runtimeSpec.Execute.Args); - Assert.Contains("npx --no-install tsx --tsconfig tsconfig.apphost.json {appHostFile}", watchExecute.Args); + Assert.Contains("npx --no-install tsc --noEmit -p tsconfig.apphost.json && npx --no-install tsx --tsconfig tsconfig.apphost.json \"{appHostFile}\"", watchExecute.Args); } private static JsonObject ParseJson(string content) => JsonNode.Parse(content)!.AsObject();