diff --git a/src/Aspire.Cli/Projects/TypeScriptAppHostToolchainResolver.cs b/src/Aspire.Cli/Projects/TypeScriptAppHostToolchainResolver.cs index 407ce55fe9a..ea1c245a40f 100644 --- a/src/Aspire.Cli/Projects/TypeScriptAppHostToolchainResolver.cs +++ b/src/Aspire.Cli/Projects/TypeScriptAppHostToolchainResolver.cs @@ -15,7 +15,8 @@ internal enum TypeScriptAppHostToolchain Npm, Bun, Yarn, - Pnpm + Pnpm, + Deno } internal static class TypeScriptAppHostToolchainResolver @@ -28,6 +29,9 @@ internal static class TypeScriptAppHostToolchainResolver private const string YarnConfigFileName = ".yarnrc.yml"; private const string PackageLockFileName = "package-lock.json"; private const string PnpmLockFileName = "pnpm-lock.yaml"; + private const string DenoLockFileName = "deno.lock"; + private const string DenoJsonFileName = "deno.json"; + private const string DenoJsoncFileName = "deno.jsonc"; public static bool IsTypeScriptLanguage(LanguageInfo? language) { @@ -87,6 +91,21 @@ internal static TypeScriptAppHostToolchainResolution ResolveWithReason(Directory return CreateLockFileResolution(TypeScriptAppHostToolchain.Yarn, YarnConfigFileName, candidateDirectory); } + if (File.Exists(Path.Combine(candidateDirectory.FullName, DenoLockFileName))) + { + return CreateLockFileResolution(TypeScriptAppHostToolchain.Deno, DenoLockFileName, candidateDirectory); + } + + if (File.Exists(Path.Combine(candidateDirectory.FullName, DenoJsonFileName))) + { + return CreateLockFileResolution(TypeScriptAppHostToolchain.Deno, DenoJsonFileName, candidateDirectory); + } + + if (File.Exists(Path.Combine(candidateDirectory.FullName, DenoJsoncFileName))) + { + return CreateLockFileResolution(TypeScriptAppHostToolchain.Deno, DenoJsoncFileName, candidateDirectory); + } + if (File.Exists(Path.Combine(candidateDirectory.FullName, PackageLockFileName))) { return CreateLockFileResolution(TypeScriptAppHostToolchain.Npm, PackageLockFileName, candidateDirectory); @@ -113,6 +132,7 @@ public static string GetCommandName(TypeScriptAppHostToolchain toolchain) TypeScriptAppHostToolchain.Bun => "bun", TypeScriptAppHostToolchain.Yarn => "yarn", TypeScriptAppHostToolchain.Pnpm => "pnpm", + TypeScriptAppHostToolchain.Deno => "deno", _ => throw new ArgumentOutOfRangeException(nameof(toolchain), toolchain, null) }; } @@ -130,6 +150,7 @@ public static string GetDisplayName(TypeScriptAppHostToolchain toolchain) TypeScriptAppHostToolchain.Bun => "Bun", TypeScriptAppHostToolchain.Yarn => "Yarn", TypeScriptAppHostToolchain.Pnpm => "pnpm", + TypeScriptAppHostToolchain.Deno => "Deno", _ => throw new ArgumentOutOfRangeException(nameof(toolchain), toolchain, null) }; } @@ -198,6 +219,13 @@ private static CommandSpec[] CreatePreExecuteCommands(TypeScriptAppHostToolchain Command = "pnpm", Args = ["exec", "tsc", "--noEmit", "-p", tsConfigFileName] }, + // Deno type-checks with its own compiler and config (deno.json), so there is no + // tsc/tsconfig step. `deno check` is the native no-emit type-check over the AppHost graph. + TypeScriptAppHostToolchain.Deno => new CommandSpec + { + Command = "deno", + Args = ["check", "{appHostFile}"] + }, _ => throw new ArgumentOutOfRangeException(nameof(toolchain), toolchain, null) } ]; @@ -222,6 +250,15 @@ private static CommandSpec CreateExecuteCommand(TypeScriptAppHostToolchain toolc Command = "pnpm", Args = ["exec", "tsx", "--tsconfig", tsConfigFileName, "{appHostFile}"] }, + // Deno runs the AppHost as its own runtime (no tsx transpiler). Unlike Node/Bun, Deno is + // not permissive for package.json projects: APIs like Deno.env/Deno.serve throw NotCapable + // without flags (and error non-interactively rather than prompting). The AppHost needs + // full host access, so `-A` grants all permissions, mirroring how Node/Bun run unrestricted. + TypeScriptAppHostToolchain.Deno => new CommandSpec + { + Command = "deno", + Args = ["run", "-A", "{appHostFile}"] + }, _ => throw new ArgumentOutOfRangeException(nameof(toolchain), toolchain, null) }; } @@ -275,6 +312,14 @@ private static CommandSpec CreateWatchCommand(TypeScriptAppHostToolchain toolcha "--exec", $"pnpm exec tsc --noEmit -p {tsConfigFileName} && pnpm exec tsx --tsconfig {tsConfigFileName} \"{{appHostFile}}\"" ] }, + // Deno has a native file watcher, so nodemon is unnecessary. `--check` makes each restart + // type-check before running, matching the nodemon "tsc --noEmit && run" behavior other + // toolchains emulate. `-A` grants full permissions as on the non-watch execute command. + TypeScriptAppHostToolchain.Deno => new CommandSpec + { + Command = "deno", + Args = ["run", "-A", "--check", "--watch", "{appHostFile}"] + }, _ => throw new ArgumentOutOfRangeException(nameof(toolchain), toolchain, null) }; } @@ -344,6 +389,7 @@ private static bool TryParseToolchain(string packageManagerName, out TypeScriptA "bun" => TypeScriptAppHostToolchain.Bun, "yarn" => TypeScriptAppHostToolchain.Yarn, "pnpm" => TypeScriptAppHostToolchain.Pnpm, + "deno" => TypeScriptAppHostToolchain.Deno, _ => null }; diff --git a/src/Aspire.Cli/Scaffolding/ScaffoldingService.cs b/src/Aspire.Cli/Scaffolding/ScaffoldingService.cs index 5aca9475e51..908671703af 100644 --- a/src/Aspire.Cli/Scaffolding/ScaffoldingService.cs +++ b/src/Aspire.Cli/Scaffolding/ScaffoldingService.cs @@ -382,6 +382,9 @@ private static string CreateRootDelegateScript(TypeScriptAppHostToolchain toolch TypeScriptAppHostToolchain.Pnpm => $"pnpm --dir {relativeAppHostDirectory} run {scriptName}", TypeScriptAppHostToolchain.Yarn => $"yarn --cwd {relativeAppHostDirectory} run {scriptName}", TypeScriptAppHostToolchain.Bun => $"bun --cwd {relativeAppHostDirectory} run {scriptName}", + // Deno has no `run