File: Templating\CliTemplateFactory.TypeScriptStarterTemplate.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Cli.Interaction;
using Microsoft.Extensions.Logging;
using Spectre.Console;
 
namespace Aspire.Cli.Templating;
 
internal sealed partial class CliTemplateFactory
{
    private async Task<TemplateResult> ApplyTypeScriptStarterTemplateAsync(CallbackTemplate _, TemplateInputs inputs, System.CommandLine.ParseResult parseResult, CancellationToken cancellationToken)
    {
        var projectName = inputs.Name;
        if (string.IsNullOrWhiteSpace(projectName))
        {
            var defaultName = _executionContext.WorkingDirectory.Name;
            projectName = await _prompter.PromptForProjectNameAsync(defaultName, cancellationToken);
        }
 
        if (string.IsNullOrWhiteSpace(inputs.Version))
        {
            _interactionService.DisplayError("Unable to determine Aspire version for the TypeScript starter template.");
            return new TemplateResult(ExitCodeConstants.InvalidCommand);
        }
 
        var aspireVersion = inputs.Version;
        var outputPath = inputs.Output;
        if (string.IsNullOrWhiteSpace(outputPath))
        {
            var defaultOutputPath = $"./{projectName}";
            outputPath = await _prompter.PromptForOutputPath(defaultOutputPath, cancellationToken);
        }
        if (!Path.IsPathRooted(outputPath))
        {
            outputPath = Path.Combine(_executionContext.WorkingDirectory.FullName, outputPath);
        }
 
        _logger.LogDebug("Applying TypeScript starter template. ProjectName: {ProjectName}, OutputPath: {OutputPath}, AspireVersion: {AspireVersion}.", projectName, outputPath, aspireVersion);
 
        var useLocalhostTld = await ResolveUseLocalhostTldAsync(parseResult, cancellationToken);
 
        try
        {
            if (!Directory.Exists(outputPath))
            {
                Directory.CreateDirectory(outputPath);
            }
 
            var projectNameLower = projectName.ToLowerInvariant();
 
            // Generate random ports (matching .NET template port ranges)
            var ports = GenerateRandomPorts();
            var hostName = useLocalhostTld ? $"{projectNameLower}.dev.localhost" : "localhost";
            string ApplyAllTokens(string content) => ApplyTokens(content, projectName, projectNameLower, aspireVersion, ports, hostName);
            _logger.LogDebug("Copying embedded TypeScript starter template files to '{OutputPath}'.", outputPath);
            await CopyTemplateTreeToDiskAsync("ts-starter", outputPath, ApplyAllTokens, cancellationToken);
        }
        catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
        {
            _interactionService.DisplayError($"Failed to create project files: {ex.Message}");
            return new TemplateResult(ExitCodeConstants.FailedToCreateNewProject);
        }
 
        var npmPath = PathLookupHelper.FindFullPathFromPath("npm") ?? PathLookupHelper.FindFullPathFromPath("npm.cmd");
        if (npmPath is null)
        {
            _interactionService.DisplayError("npm is not installed or not found in PATH. Please install Node.js and try again.");
            return new TemplateResult(ExitCodeConstants.InvalidCommand);
        }
 
        // Run npm install in the output directory (non-fatal — package may not be published yet)
        _logger.LogDebug("Running npm install for TypeScript starter in '{OutputPath}'.", outputPath);
        var npmInstallResult = await RunProcessAsync(npmPath, "install", outputPath, cancellationToken);
        if (npmInstallResult.ExitCode != 0)
        {
            _interactionService.DisplaySubtleMessage("npm install had warnings or errors. You may need to run 'npm install' manually after dependencies are available.");
            DisplayProcessOutput(npmInstallResult, treatStandardErrorAsError: false);
        }
 
        _interactionService.DisplaySuccess($"Created TypeScript starter project at {outputPath.EscapeMarkup()}");
        _interactionService.DisplayMessage(KnownEmojis.Information, "Run 'aspire run' to start your AppHost.");
 
        return new TemplateResult(ExitCodeConstants.Success, outputPath);
    }
}