File: PackageInstallationTests.cs
Web Access
Project: src\tests\Aspire.Hosting.JavaScript.Tests\Aspire.Hosting.JavaScript.Tests.csproj (Aspire.Hosting.JavaScript.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Runtime.CompilerServices;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Tests.Utils;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;
 
namespace Aspire.Hosting.JavaScript.Tests;
 
public class PackageInstallationTests
{
    /// <summary>
    /// This test validates that the WithNpm method creates
    /// installer resources with proper arguments and relationships.
    /// </summary>
    [Fact]
    public void WithNpm_CanBeConfiguredWithInstall()
    {
        var builder = DistributedApplication.CreateBuilder();
 
        var nodeApp = builder.AddJavaScriptApp("nodeApp", "./test-app");
        var nodeApp2 = builder.AddJavaScriptApp("nodeApp2", "./test-app-2");
 
        // Test that both configurations can be set up without errors
        nodeApp.WithNpm(install: true); // Uses npm install
        nodeApp2.WithNpm(install: false);
 
        using var app = builder.Build();
 
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
        var nodeResources = appModel.Resources.OfType<JavaScriptAppResource>().ToList();
        var installerResources = appModel.Resources.OfType<JavaScriptInstallerResource>().ToList();
 
        Assert.Equal(2, nodeResources.Count);
        Assert.Single(installerResources);
        Assert.All(nodeResources, resource => Assert.Equal("npm", resource.Command));
 
        // Verify the installer exists for nodeApp
        var nodeAppInstallResource = installerResources.Single(r => r.Name == "nodeApp-installer");
        Assert.NotNull(nodeAppInstallResource);
 
        // Verify no installer for nodeApp2
        var nodeApp2InstallResource = installerResources.SingleOrDefault(r => r.Name == "nodeApp2-installer");
        Assert.Null(nodeApp2InstallResource);
    }
 
    [Fact]
    public void WithNpm_ExcludedFromPublishMode()
    {
        var builder = DistributedApplication.CreateBuilder(["Publishing:Publisher=manifest", "Publishing:OutputPath=./publish"]);
 
        var nodeApp = builder.AddJavaScriptApp("test-app", "./test-app");
        nodeApp.WithNpm(install: true);
 
        using var app = builder.Build();
 
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Verify the JavaScriptApp resource exists
        var nodeResource = Assert.Single(appModel.Resources, r => r.Name == "test-app");
 
        // Verify NO installer resource was created in publish mode
        var installerResources = appModel.Resources.OfType<JavaScriptInstallerResource>().ToList();
        Assert.Empty(installerResources);
 
        // Verify no wait annotations were added
        Assert.False(nodeResource.TryGetAnnotationsOfType<WaitAnnotation>(out _));
    }
 
    [Fact]
    public async Task WithYarn_CreatesInstallerWhenInstallIsTrue()
    {
        var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
 
        var nodeApp = builder.AddJavaScriptApp("test-app", "./test-app");
        nodeApp.WithYarn(install: true);
 
        using var app = builder.Build();
        await ExecuteBeforeStartHooksAsync(app, CancellationToken.None);
 
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Verify the JavaScriptApp resource exists with yarn command
        var nodeResource = Assert.Single(appModel.Resources.OfType<JavaScriptAppResource>());
        Assert.Equal("yarn", nodeResource.Command);
 
        // Verify the package manager annotation
        Assert.True(nodeResource.TryGetLastAnnotation<JavaScriptPackageManagerAnnotation>(out var packageManager));
        Assert.Equal("yarn", packageManager.ExecutableName);
        Assert.Equal("run", packageManager.ScriptCommand);
 
        // Verify the install command annotation
        Assert.True(nodeResource.TryGetLastAnnotation<JavaScriptInstallCommandAnnotation>(out var installAnnotation));
        Assert.Equal(["install"], installAnnotation.Args);
 
        // Verify the run command annotation
        Assert.True(nodeResource.TryGetLastAnnotation<JavaScriptRunScriptAnnotation>(out var runAnnotation));
        Assert.Equal("dev", runAnnotation.ScriptName);
 
        // Verify the build command annotation
        Assert.True(nodeResource.TryGetLastAnnotation<JavaScriptBuildScriptAnnotation>(out var buildAnnotation));
        Assert.Equal("build", buildAnnotation.ScriptName);
 
        // Verify the installer resource was created
        var installerResource = Assert.Single(appModel.Resources.OfType<JavaScriptInstallerResource>());
        Assert.Equal("test-app-installer", installerResource.Name);
    }
 
    [Fact]
    public async Task WithYarn_DoesNotCreateInstallerWhenInstallIsFalse()
    {
        var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
 
        var nodeApp = builder.AddJavaScriptApp("test-app", "./test-app");
        nodeApp.WithYarn(install: false);
 
        using var app = builder.Build();
        await ExecuteBeforeStartHooksAsync(app, CancellationToken.None);
 
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Verify the JavaScriptApp resource exists with yarn command
        var nodeResource = Assert.Single(appModel.Resources.OfType<JavaScriptAppResource>());
        Assert.Equal("yarn", nodeResource.Command);
 
        // Verify annotations are set
        Assert.True(nodeResource.TryGetLastAnnotation<JavaScriptInstallCommandAnnotation>(out var _));
        Assert.True(nodeResource.TryGetLastAnnotation<JavaScriptRunScriptAnnotation>(out var _));
        Assert.True(nodeResource.TryGetLastAnnotation<JavaScriptBuildScriptAnnotation>(out var _));
 
        // Verify NO installer resource was created
        var installerResources = appModel.Resources.OfType<JavaScriptInstallerResource>().ToList();
        Assert.Empty(installerResources);
    }
 
    [Fact]
    public async Task WithPnpm_CreatesInstallerWhenInstallIsTrue()
    {
        var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
 
        var nodeApp = builder.AddJavaScriptApp("test-app", "./test-app");
        nodeApp.WithPnpm(install: true);
 
        using var app = builder.Build();
        await ExecuteBeforeStartHooksAsync(app, CancellationToken.None);
 
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Verify the JavaScriptApp resource exists with pnpm command
        var nodeResource = Assert.Single(appModel.Resources.OfType<JavaScriptAppResource>());
        Assert.Equal("pnpm", nodeResource.Command);
 
        Assert.True(nodeResource.TryGetLastAnnotation<JavaScriptPackageManagerAnnotation>(out var packageManager));
        Assert.Equal("pnpm", packageManager.ExecutableName);
        Assert.Equal("run", packageManager.ScriptCommand);
 
        // Verify the install command annotation
        Assert.True(nodeResource.TryGetLastAnnotation<JavaScriptInstallCommandAnnotation>(out var installAnnotation));
        Assert.Equal(["install"], installAnnotation.Args);
 
        // Verify the run command annotation
        Assert.True(nodeResource.TryGetLastAnnotation<JavaScriptRunScriptAnnotation>(out var runAnnotation));
        Assert.Equal("dev", runAnnotation.ScriptName);
 
        // Verify the build command annotation
        Assert.True(nodeResource.TryGetLastAnnotation<JavaScriptBuildScriptAnnotation>(out var buildAnnotation));
        Assert.Equal("build", buildAnnotation.ScriptName);
        Assert.Empty(buildAnnotation.Args);
 
        // Verify the installer resource was created
        var installerResource = Assert.Single(appModel.Resources.OfType<JavaScriptInstallerResource>());
        Assert.Equal("test-app-installer", installerResource.Name);
    }
 
    [Fact]
    public async Task WithPnpm_DoesNotCreateInstallerWhenInstallIsFalse()
    {
        var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
 
        var nodeApp = builder.AddJavaScriptApp("test-app", "./test-app");
        nodeApp.WithPnpm(install: false);
 
        using var app = builder.Build();
        await ExecuteBeforeStartHooksAsync(app, CancellationToken.None);
 
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Verify the JavaScriptApp resource exists with pnpm command
        var nodeResource = Assert.Single(appModel.Resources.OfType<JavaScriptAppResource>());
        Assert.Equal("pnpm", nodeResource.Command);
 
        // Verify annotations are set
        Assert.True(nodeResource.TryGetLastAnnotation<JavaScriptInstallCommandAnnotation>(out var _));
        Assert.True(nodeResource.TryGetLastAnnotation<JavaScriptRunScriptAnnotation>(out var _));
        Assert.True(nodeResource.TryGetLastAnnotation<JavaScriptBuildScriptAnnotation>(out var _));
 
        // Verify NO installer resource was created
        var installerResources = appModel.Resources.OfType<JavaScriptInstallerResource>().ToList();
        Assert.Empty(installerResources);
    }
 
    [Fact]
    public void WithNpm_CreatesInstallerWithCustomCommand()
    {
        var builder = DistributedApplication.CreateBuilder();
 
        var nodeApp = builder.AddJavaScriptApp("test-app", "./test-app");
        nodeApp.WithNpm(installCommand: "ci", installArgs: ["--no-fund"]);
 
        using var app = builder.Build();
 
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Verify the JavaScriptApp resource exists
        var nodeResource = Assert.Single(appModel.Resources.OfType<JavaScriptAppResource>());
 
        // Verify the install command annotation with custom command
        Assert.True(nodeResource.TryGetLastAnnotation<JavaScriptInstallCommandAnnotation>(out var installAnnotation));
        Assert.Equal(["ci", "--no-fund"], installAnnotation.Args);
 
        // Verify the installer resource was created
        var installerResource = Assert.Single(appModel.Resources.OfType<JavaScriptInstallerResource>());
        Assert.Equal("test-app-installer", installerResource.Name);
    }
 
    [Fact]
    public void WithBuildScript_SetsCustomBuildCommand()
    {
        var builder = DistributedApplication.CreateBuilder();
 
        var nodeApp = builder.AddJavaScriptApp("test-app", "./test-app");
        nodeApp.WithBuildScript("bun", ["run", "build:prod"]);
 
        using var app = builder.Build();
 
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Verify the JavaScriptApp resource exists
        var nodeResource = Assert.Single(appModel.Resources.OfType<JavaScriptAppResource>());
 
        // Verify the build command annotation with custom command
        Assert.True(nodeResource.TryGetLastAnnotation<JavaScriptBuildScriptAnnotation>(out var buildAnnotation));
        Assert.Equal("bun", buildAnnotation.ScriptName);
        Assert.Equal(["run", "build:prod"], buildAnnotation.Args);
    }
 
    [Fact]
    public async Task WithRunScript_SetsCustomRunCommand()
    {
        var builder = DistributedApplication.CreateBuilder();
 
        var nodeApp = builder.AddJavaScriptApp("test-app", "./test-app");
        nodeApp.WithRunScript("start", ["--my-args"]);
 
        using var app = builder.Build();
 
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Verify the JavaScriptApp resource exists
        var nodeResource = Assert.Single(appModel.Resources.OfType<JavaScriptAppResource>());
 
        var args = await ArgumentEvaluator.GetArgumentListAsync(nodeResource);
 
        Assert.Collection(args,
            arg => Assert.Equal("run", arg),
            arg => Assert.Equal("start", arg),
            arg => Assert.Equal("--my-args", arg));
    }
 
    [Fact]
    public void WithNpmInstallWithYarnNoInstall()
    {
        var builder = DistributedApplication.CreateBuilder();
 
        builder.AddViteApp("test-app", "./test-app")
            .WithNpm(install: true)
            .WithYarn(install: false);
 
        using var app = builder.Build();
 
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Verify the JavaScriptApp resource exists
        var nodeResource = Assert.Single(appModel.Resources.OfType<JavaScriptAppResource>());
 
        Assert.True(nodeResource.TryGetLastAnnotation<JavaScriptPackageManagerAnnotation>(out var packageManager));
        Assert.Equal("yarn", packageManager.ExecutableName);
 
        // Verify the install command annotation is correct - it should still be there
        Assert.True(nodeResource.TryGetLastAnnotation<JavaScriptInstallCommandAnnotation>(out var installAnnotation));
        Assert.Equal(["install"], installAnnotation.Args);
 
        // the installer resource should NOT be created
        Assert.Empty(appModel.Resources.OfType<JavaScriptInstallerResource>());
    }
 
    [Fact]
    public void WithNpmNoInstallWithYarnInstall()
    {
        var builder = DistributedApplication.CreateBuilder();
 
        builder.AddViteApp("test-app", "./test-app")
            .WithNpm(install: false)
            .WithYarn(install: true);
 
        using var app = builder.Build();
 
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Verify the JavaScriptApp resource exists
        var nodeResource = Assert.Single(appModel.Resources.OfType<JavaScriptAppResource>());
 
        Assert.True(nodeResource.TryGetLastAnnotation<JavaScriptPackageManagerAnnotation>(out var packageManager));
        Assert.Equal("yarn", packageManager.ExecutableName);
 
        // Verify the install command annotation is correct - it should still be there
        Assert.True(nodeResource.TryGetLastAnnotation<JavaScriptInstallCommandAnnotation>(out var installAnnotation));
        Assert.Equal(["install"], installAnnotation.Args);
 
        // the installer resource should be created
        Assert.Single(appModel.Resources.OfType<JavaScriptInstallerResource>());
    }
 
    [Fact]
    public async Task WithNpmInstallWithYarnInstall()
    {
        var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
 
        builder.AddViteApp("test-app", "./test-app")
            .WithNpm(install: true)
            .WithYarn(install: true);
 
        using var app = builder.Build();
        await ExecuteBeforeStartHooksAsync(app, CancellationToken.None);
 
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Verify the JavaScriptApp resource exists
        var nodeResource = Assert.Single(appModel.Resources.OfType<JavaScriptAppResource>());
 
        Assert.True(nodeResource.TryGetLastAnnotation<JavaScriptPackageManagerAnnotation>(out var packageManager));
        Assert.Equal("yarn", packageManager.ExecutableName);
 
        // Verify the install command annotation is correct - it should still be there
        Assert.True(nodeResource.TryGetLastAnnotation<JavaScriptInstallCommandAnnotation>(out var installAnnotation));
        Assert.Equal(["install"], installAnnotation.Args);
 
        // a single installer resource should be created
        var installer = Assert.Single(appModel.Resources.OfType<JavaScriptInstallerResource>());
        Assert.Equal("yarn", installer.Command);
    }
 
    [Fact]
    public void WithNpm_DefaultInstallsPackages()
    {
        var builder = DistributedApplication.CreateBuilder();
 
        var nodeApp = builder.AddJavaScriptApp("test-app", "./test-app");
        nodeApp.WithNpm(); // Using default parameter (should be install: true)
 
        using var app = builder.Build();
 
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Verify the JavaScriptApp resource exists with npm command
        var nodeResource = Assert.Single(appModel.Resources.OfType<JavaScriptAppResource>());
        Assert.Equal("npm", nodeResource.Command);
 
        // Verify the installer resource was created by default
        var installerResource = Assert.Single(appModel.Resources.OfType<JavaScriptInstallerResource>());
        Assert.Equal("test-app-installer", installerResource.Name);
    }
 
    [Fact]
    public async Task WithYarn_DefaultInstallsPackages()
    {
        var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
 
        var nodeApp = builder.AddJavaScriptApp("test-app", "./test-app");
        nodeApp.WithYarn(); // Using default parameter (should be install: true)
 
        using var app = builder.Build();
        await ExecuteBeforeStartHooksAsync(app, CancellationToken.None);
 
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Verify the JavaScriptApp resource exists with yarn command
        var nodeResource = Assert.Single(appModel.Resources.OfType<JavaScriptAppResource>());
        Assert.Equal("yarn", nodeResource.Command);
 
        // Verify the installer resource was created by default
        var installerResource = Assert.Single(appModel.Resources.OfType<JavaScriptInstallerResource>());
        Assert.Equal("test-app-installer", installerResource.Name);
    }
 
    [Fact]
    public async Task WithPnpm_DefaultInstallsPackages()
    {
        var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
 
        var nodeApp = builder.AddJavaScriptApp("test-app", "./test-app");
        nodeApp.WithPnpm(); // Using default parameter (should be install: true)
 
        using var app = builder.Build();
        await ExecuteBeforeStartHooksAsync(app, CancellationToken.None);
 
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Verify the JavaScriptApp resource exists with pnpm command
        var nodeResource = Assert.Single(appModel.Resources.OfType<JavaScriptAppResource>());
        Assert.Equal("pnpm", nodeResource.Command);
 
        // Verify the installer resource was created by default
        var installerResource = Assert.Single(appModel.Resources.OfType<JavaScriptInstallerResource>());
        Assert.Equal("test-app-installer", installerResource.Name);
    }
 
    [Fact]
    public void AddViteApp_DefaultInstallsPackages()
    {
        var builder = DistributedApplication.CreateBuilder();
 
        builder.AddViteApp("test-app", "./test-app");
 
        using var app = builder.Build();
 
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Verify the JavaScriptApp resource exists with npm command (default package manager)
        var nodeResource = Assert.Single(appModel.Resources.OfType<JavaScriptAppResource>());
        Assert.Equal("npm", nodeResource.Command);
 
        // Verify the installer resource was created by default for ViteApp
        var installerResource = Assert.Single(appModel.Resources.OfType<JavaScriptInstallerResource>());
        Assert.Equal("test-app-installer", installerResource.Name);
    }
 
    [Fact]
    public void WithNpm_DefaultsArgsInPublishMode()
    {
        using var tempDir = new TempDirectory();
        File.WriteAllText(Path.Combine(tempDir.Path, "package-lock.json"), "empty");
 
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var app = builder.AddViteApp("test-app", tempDir.Path)
            .WithNpm();
 
        Assert.True(app.Resource.TryGetLastAnnotation<JavaScriptInstallCommandAnnotation>(out var installCommand));
        Assert.Equal(["ci"], installCommand.Args);
    }
 
    [Fact]
    public void WithNpm_CanChangeInstallCommandAndArgs()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var app = builder.AddViteApp("test-app", "./test-app")
            .WithNpm(installCommand: "myinstall", installArgs: ["--no-fund"]);
 
        Assert.True(app.Resource.TryGetLastAnnotation<JavaScriptInstallCommandAnnotation>(out var installCommand));
        Assert.Equal(["myinstall", "--no-fund"], installCommand.Args);
    }
 
    [Fact]
    public void WithYarn_DefaultsArgsInPublishMode()
    {
        using var tempDir = new TempDirectory();
        File.WriteAllText(Path.Combine(tempDir.Path, "yarn.lock"), "empty");
 
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var app = builder.AddViteApp("test-app", tempDir.Path)
            .WithYarn();
 
        Assert.True(app.Resource.TryGetLastAnnotation<JavaScriptInstallCommandAnnotation>(out var installCommand));
        Assert.Equal(["install", "--frozen-lockfile"], installCommand.Args);
 
        var app2 = builder.AddViteApp("test-app2", tempDir.Path)
            .WithYarn(installArgs: ["--immutable-cache"]);
 
        Assert.True(app2.Resource.TryGetLastAnnotation<JavaScriptInstallCommandAnnotation>(out installCommand));
        Assert.Equal(["install", "--immutable-cache"], installCommand.Args);
    }
 
    [Fact]
    public void WithYarn_ReturnsImmutable_WhenYarnRcYmlExists()
    {
        using var tempDir = new TempDirectory();
        File.WriteAllText(Path.Combine(tempDir.Path, "yarn.lock"), "empty");
        File.WriteAllText(Path.Combine(tempDir.Path, ".yarnrc.yml"), "empty");
 
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var app = builder.AddViteApp("test-app", tempDir.Path)
            .WithYarn();
 
        Assert.True(app.Resource.TryGetLastAnnotation<JavaScriptInstallCommandAnnotation>(out var installCommand));
        Assert.Equal(["install", "--immutable"], installCommand.Args);
    }
 
    [Fact]
    public void WithYarn_ReturnsImmutable_WhenYarnReleasesDirExists()
    {
        using var tempDir = new TempDirectory();
        File.WriteAllText(Path.Combine(tempDir.Path, "yarn.lock"), "empty");
        Directory.CreateDirectory(Path.Combine(tempDir.Path, ".yarn", "releases"));
 
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var app = builder.AddViteApp("test-app", tempDir.Path)
            .WithYarn();
 
        Assert.True(app.Resource.TryGetLastAnnotation<JavaScriptInstallCommandAnnotation>(out var installCommand));
        Assert.Equal(["install", "--immutable"], installCommand.Args);
    }
 
    [Fact]
    public void WithPnpm_DefaultsArgsInPublishMode()
    {
        using var tempDir = new TempDirectory();
        File.WriteAllText(Path.Combine(tempDir.Path, "pnpm-lock.yaml"), "empty");
 
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var app = builder.AddViteApp("test-app", tempDir.Path)
            .WithPnpm();
 
        Assert.True(app.Resource.TryGetLastAnnotation<JavaScriptInstallCommandAnnotation>(out var installCommand));
        Assert.Equal(["install", "--frozen-lockfile"], installCommand.Args);
    }
 
    [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ExecuteBeforeStartHooksAsync")]
    private static extern Task ExecuteBeforeStartHooksAsync(DistributedApplication app, CancellationToken cancellationToken);
}