File: TypeScriptLanguageSupport.cs
Web Access
Project: src\src\Aspire.Hosting.CodeGeneration.TypeScript\Aspire.Hosting.CodeGeneration.TypeScript.csproj (Aspire.Hosting.CodeGeneration.TypeScript)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Hosting.Ats;
 
namespace Aspire.Hosting.CodeGeneration.TypeScript;
 
/// <summary>
/// Provides language support for TypeScript AppHosts.
/// Implements scaffolding, detection, and runtime configuration.
/// </summary>
public sealed class TypeScriptLanguageSupport : ILanguageSupport
{
    /// <summary>
    /// The language/runtime identifier for TypeScript with Node.js.
    /// Format: {language}/{runtime} to support multiple runtimes (e.g., typescript/bun, typescript/deno).
    /// </summary>
    private const string LanguageId = "typescript/nodejs";
 
    /// <summary>
    /// The code generation target language. This maps to the ICodeGenerator.Language property.
    /// </summary>
    private const string CodeGenTarget = "TypeScript";
 
    private const string LanguageDisplayName = "TypeScript (Node.js)";
    private static readonly string[] s_detectionPatterns = ["apphost.ts"];
 
    /// <inheritdoc />
    public string Language => LanguageId;
 
    /// <inheritdoc />
    public Dictionary<string, string> Scaffold(ScaffoldRequest request)
    {
        var files = new Dictionary<string, string>();
 
        // Create apphost.ts
        files["apphost.ts"] = """
            // Aspire TypeScript AppHost
            // For more information, see: https://aspire.dev
 
            import { createBuilder } from './.modules/aspire.js';
 
            const builder = await createBuilder();
 
            // Add your resources here, for example:
            // const redis = await builder.addContainer("cache", "redis:latest");
            // const postgres = await builder.addPostgres("db");
 
            await builder.build().run();
            """;
 
        // Create package.json
        var packageName = request.ProjectName?.ToLowerInvariant() ?? "aspire-apphost";
        files["package.json"] = $$"""
            {
              "name": "{{packageName}}",
              "version": "1.0.0",
              "type": "module",
              "scripts": {
                "start": "aspire run",
                "build": "tsc",
                "dev": "tsc --watch"
              },
              "dependencies": {
                "vscode-jsonrpc": "^8.2.0"
              },
              "devDependencies": {
                "@types/node": "^20.0.0",
                "nodemon": "^3.1.11",
                "tsx": "^4.19.0",
                "typescript": "^5.3.0"
              }
            }
            """;
 
        // Create tsconfig.json for TypeScript configuration
        files["tsconfig.json"] = """
            {
              "compilerOptions": {
                "target": "ES2022",
                "module": "NodeNext",
                "moduleResolution": "NodeNext",
                "esModuleInterop": true,
                "forceConsistentCasingInFileNames": true,
                "strict": true,
                "skipLibCheck": true,
                "outDir": "./dist",
                "rootDir": "."
              },
              "include": ["apphost.ts", ".modules/**/*.ts"],
              "exclude": ["node_modules"]
            }
            """;
 
        // Create apphost.run.json with random ports
        // Use PortSeed if provided (for testing), otherwise use random
        var random = request.PortSeed.HasValue
            ? new Random(request.PortSeed.Value)
            : Random.Shared;
 
        var httpsPort = random.Next(10000, 65000);
        var httpPort = random.Next(10000, 65000);
        var otlpPort = random.Next(10000, 65000);
        var resourceServicePort = random.Next(10000, 65000);
 
        files["apphost.run.json"] = $$"""
            {
              "profiles": {
                "https": {
                  "applicationUrl": "https://localhost:{{httpsPort}};http://localhost:{{httpPort}}",
                  "environmentVariables": {
                    "ASPNETCORE_ENVIRONMENT": "Development",
                    "DOTNET_ENVIRONMENT": "Development",
                    "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:{{otlpPort}}",
                    "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:{{resourceServicePort}}"
                  }
                }
              }
            }
            """;
 
        return files;
    }
 
    /// <inheritdoc />
    public DetectionResult Detect(string directoryPath)
    {
        // Check for apphost.ts
        var appHostPath = Path.Combine(directoryPath, "apphost.ts");
        if (!File.Exists(appHostPath))
        {
            return DetectionResult.NotFound;
        }
 
        // Check for package.json (required for TypeScript/Node.js projects)
        var packageJsonPath = Path.Combine(directoryPath, "package.json");
        if (!File.Exists(packageJsonPath))
        {
            return DetectionResult.NotFound;
        }
 
        // Note: .csproj precedence is handled by the CLI, not here.
        // Language support should only check for its own language markers.
 
        return DetectionResult.Found(LanguageId, "apphost.ts");
    }
 
    /// <inheritdoc />
    public RuntimeSpec GetRuntimeSpec()
    {
        return new RuntimeSpec
        {
            Language = LanguageId,
            DisplayName = LanguageDisplayName,
            CodeGenLanguage = CodeGenTarget,
            DetectionPatterns = s_detectionPatterns,
            InstallDependencies = new CommandSpec
            {
                Command = "npm",
                Args = ["install"]
            },
            Execute = new CommandSpec
            {
                Command = "npx",
                Args = ["tsx", "{appHostFile}"]
            },
            WatchExecute = new CommandSpec
            {
                Command = "npx",
                Args = [
                    "nodemon",
                    "--signal", "SIGTERM",
                    "--watch", ".",
                    "--ext", "ts",
                    "--ignore", "node_modules/",
                    "--ignore", ".modules/",
                    "--exec", "npx tsx {appHostFile}"
                ]
            }
        };
    }
}