From d5f193bc3ed4dc162b6fcba3901cf1b1912bcf16 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Jun 2025 01:13:44 +0000 Subject: [PATCH 1/5] Initial plan for issue From 263edc37238d16c48eeedee36b48cacbec0ec867 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Jun 2025 01:20:38 +0000 Subject: [PATCH 2/5] Add args parameter to package installation methods Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> --- .../NodeJSHostingExtensions.cs | 33 ++++++-- .../PackageInstallationTests.cs | 81 +++++++++++++++++++ 2 files changed, 108 insertions(+), 6 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions/NodeJSHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions/NodeJSHostingExtensions.cs index 61f0cb9a..fdfcb290 100644 --- a/src/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions/NodeJSHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions/NodeJSHostingExtensions.cs @@ -110,8 +110,9 @@ public static IResourceBuilder AddPnpmApp(this IDistributedAppl /// /// The Node.js app resource. /// When true use npm ci otherwise use npm install when installing packages. + /// Additional arguments to pass to the npm command. /// A reference to the . - public static IResourceBuilder WithNpmPackageInstallation(this IResourceBuilder resource, bool useCI = false) + public static IResourceBuilder WithNpmPackageInstallation(this IResourceBuilder resource, bool useCI = false, string[]? args = null) { // Only install packages during development, not in publish mode if (!resource.ApplicationBuilder.ExecutionContext.IsPublishMode) @@ -119,8 +120,14 @@ public static IResourceBuilder WithNpmPackageInstallation(this var installerName = $"{resource.Resource.Name}-npm-install"; var installer = new NpmInstallerResource(installerName, resource.Resource.WorkingDirectory); + var baseArgs = new List { useCI ? "ci" : "install" }; + if (args != null) + { + baseArgs.AddRange(args); + } + var installerBuilder = resource.ApplicationBuilder.AddResource(installer) - .WithArgs([useCI ? "ci" : "install"]) + .WithArgs(baseArgs.ToArray()) .WithParentRelationship(resource.Resource) .ExcludeFromManifest(); @@ -135,8 +142,9 @@ public static IResourceBuilder WithNpmPackageInstallation(this /// Ensures the Node.js packages are installed before the application starts using yarn as the package manager. /// /// The Node.js app resource. + /// Additional arguments to pass to the yarn command. /// A reference to the . - public static IResourceBuilder WithYarnPackageInstallation(this IResourceBuilder resource) + public static IResourceBuilder WithYarnPackageInstallation(this IResourceBuilder resource, string[]? args = null) { // Only install packages during development, not in publish mode if (!resource.ApplicationBuilder.ExecutionContext.IsPublishMode) @@ -144,8 +152,14 @@ public static IResourceBuilder WithYarnPackageInstallation(this var installerName = $"{resource.Resource.Name}-yarn-install"; var installer = new YarnInstallerResource(installerName, resource.Resource.WorkingDirectory); + var baseArgs = new List { "install" }; + if (args != null) + { + baseArgs.AddRange(args); + } + var installerBuilder = resource.ApplicationBuilder.AddResource(installer) - .WithArgs(["install"]) + .WithArgs(baseArgs.ToArray()) .WithParentRelationship(resource.Resource) .ExcludeFromManifest(); @@ -160,8 +174,9 @@ public static IResourceBuilder WithYarnPackageInstallation(this /// Ensures the Node.js packages are installed before the application starts using pnpm as the package manager. /// /// The Node.js app resource. + /// Additional arguments to pass to the pnpm command. /// A reference to the . - public static IResourceBuilder WithPnpmPackageInstallation(this IResourceBuilder resource) + public static IResourceBuilder WithPnpmPackageInstallation(this IResourceBuilder resource, string[]? args = null) { // Only install packages during development, not in publish mode if (!resource.ApplicationBuilder.ExecutionContext.IsPublishMode) @@ -169,8 +184,14 @@ public static IResourceBuilder WithPnpmPackageInstallation(this var installerName = $"{resource.Resource.Name}-pnpm-install"; var installer = new PnpmInstallerResource(installerName, resource.Resource.WorkingDirectory); + var baseArgs = new List { "install" }; + if (args != null) + { + baseArgs.AddRange(args); + } + var installerBuilder = resource.ApplicationBuilder.AddResource(installer) - .WithArgs(["install"]) + .WithArgs(baseArgs.ToArray()) .WithParentRelationship(resource.Resource) .ExcludeFromManifest(); diff --git a/tests/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.Tests/PackageInstallationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.Tests/PackageInstallationTests.cs index b0a96cac..1c6f4d73 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.Tests/PackageInstallationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.Tests/PackageInstallationTests.cs @@ -70,4 +70,85 @@ public void WithNpmPackageInstallation_ExcludedFromPublishMode() // Verify no wait annotations were added Assert.False(nodeResource.TryGetAnnotationsOfType(out _)); } + + [Fact] + public async Task WithNpmPackageInstallation_CanAcceptAdditionalArgs() + { + var builder = DistributedApplication.CreateBuilder(); + + var nodeApp = builder.AddNpmApp("test-app", "./test-app"); + var nodeAppWithArgs = builder.AddNpmApp("test-app-args", "./test-app-args"); + + // Test npm install with additional args + nodeApp.WithNpmPackageInstallation(useCI: false, args: ["--legacy-peer-deps"]); + nodeAppWithArgs.WithNpmPackageInstallation(useCI: true, args: ["--verbose", "--no-optional"]); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var installerResources = appModel.Resources.OfType().ToList(); + + Assert.Equal(2, installerResources.Count); + + var installResource = installerResources.Single(r => r.Name == "test-app-npm-install"); + var ciResource = installerResources.Single(r => r.Name == "test-app-args-npm-install"); + + // Verify install command with additional args + var installArgs = await installResource.GetArgumentValuesAsync(); + Assert.Equal(2, installArgs.Count); + Assert.Equal("install", installArgs[0]); + Assert.Equal("--legacy-peer-deps", installArgs[1]); + + // Verify ci command with additional args + var ciArgs = await ciResource.GetArgumentValuesAsync(); + Assert.Equal(3, ciArgs.Count); + Assert.Equal("ci", ciArgs[0]); + Assert.Equal("--verbose", ciArgs[1]); + Assert.Equal("--no-optional", ciArgs[2]); + } + + [Fact] + public async Task WithYarnPackageInstallation_CanAcceptAdditionalArgs() + { + var builder = DistributedApplication.CreateBuilder(); + + var nodeApp = builder.AddYarnApp("test-yarn-app", "./test-yarn-app"); + nodeApp.WithYarnPackageInstallation(args: ["--frozen-lockfile", "--verbose"]); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var installerResources = appModel.Resources.OfType().ToList(); + + var installerResource = Assert.Single(installerResources); + Assert.Equal("test-yarn-app-yarn-install", installerResource.Name); + + var args = await installerResource.GetArgumentValuesAsync(); + Assert.Equal(3, args.Count); + Assert.Equal("install", args[0]); + Assert.Equal("--frozen-lockfile", args[1]); + Assert.Equal("--verbose", args[2]); + } + + [Fact] + public async Task WithPnpmPackageInstallation_CanAcceptAdditionalArgs() + { + var builder = DistributedApplication.CreateBuilder(); + + var nodeApp = builder.AddPnpmApp("test-pnpm-app", "./test-pnpm-app"); + nodeApp.WithPnpmPackageInstallation(args: ["--frozen-lockfile"]); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var installerResources = appModel.Resources.OfType().ToList(); + + var installerResource = Assert.Single(installerResources); + Assert.Equal("test-pnpm-app-pnpm-install", installerResource.Name); + + var args = await installerResource.GetArgumentValuesAsync(); + Assert.Equal(2, args.Count); + Assert.Equal("install", args[0]); + Assert.Equal("--frozen-lockfile", args[1]); + } } \ No newline at end of file From cb4f3b571da7ea5c2acdbf035abe357c67d946e7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Jun 2025 01:22:47 +0000 Subject: [PATCH 3/5] Add documentation and example for new args functionality Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> --- .../Program.cs | 5 +++++ .../README.md | 21 +++++++++++++++++++ ...oolkit.Aspire.Hosting.NodeJS.Extensions.cs | 6 +++--- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/examples/nodejs-ext/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.AppHost/Program.cs b/examples/nodejs-ext/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.AppHost/Program.cs index d63f79fb..ddc0ba73 100644 --- a/examples/nodejs-ext/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.AppHost/Program.cs +++ b/examples/nodejs-ext/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.AppHost/Program.cs @@ -9,4 +9,9 @@ builder.AddViteApp("pnpm-demo", packageManager: "pnpm") .WithPnpmPackageInstallation(); +// Example of using custom args - useful for legacy packages +builder.AddNpmApp("npm-with-flags", "../vite-demo") + .WithNpmPackageInstallation(useCI: false, args: ["--legacy-peer-deps"]) + .WithHttpEndpoint(env: "PORT"); + builder.Build().Run(); diff --git a/src/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions/README.md b/src/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions/README.md index 8f5b18cb..270b3c2b 100644 --- a/src/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions/README.md @@ -24,6 +24,27 @@ builder.AddPnpmApp("pnpm-demo") .WithExternalHttpEndpoints(); ``` +### Package installation with custom flags + +You can pass additional flags to package managers during installation: + +```csharp +// npm with legacy peer deps support +builder.AddNpmApp("npm-app", "./path/to/app") + .WithNpmPackageInstallation(useCI: false, args: ["--legacy-peer-deps"]) + .WithExternalHttpEndpoints(); + +// yarn with frozen lockfile +builder.AddYarnApp("yarn-app", "./path/to/app") + .WithYarnPackageInstallation(args: ["--frozen-lockfile", "--verbose"]) + .WithExternalHttpEndpoints(); + +// pnpm with frozen lockfile +builder.AddPnpmApp("pnpm-app", "./path/to/app") + .WithPnpmPackageInstallation(args: ["--frozen-lockfile"]) + .WithExternalHttpEndpoints(); +``` + ## Additional Information https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-nodejs-extensions diff --git a/src/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions/api/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.cs b/src/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions/api/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.cs index 758c1ada..73ee2eb0 100644 --- a/src/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions/api/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions/api/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.cs @@ -16,10 +16,10 @@ public static partial class NodeJSHostingExtensions public static ApplicationModel.IResourceBuilder AddYarnApp(this IDistributedApplicationBuilder builder, string name, string workingDirectory, string scriptName = "start", string[]? args = null) { throw null; } - public static ApplicationModel.IResourceBuilder WithNpmPackageInstallation(this ApplicationModel.IResourceBuilder resource, bool useCI = false) { throw null; } + public static ApplicationModel.IResourceBuilder WithNpmPackageInstallation(this ApplicationModel.IResourceBuilder resource, bool useCI = false, string[]? args = null) { throw null; } - public static ApplicationModel.IResourceBuilder WithPnpmPackageInstallation(this ApplicationModel.IResourceBuilder resource) { throw null; } + public static ApplicationModel.IResourceBuilder WithPnpmPackageInstallation(this ApplicationModel.IResourceBuilder resource, string[]? args = null) { throw null; } - public static ApplicationModel.IResourceBuilder WithYarnPackageInstallation(this ApplicationModel.IResourceBuilder resource) { throw null; } + public static ApplicationModel.IResourceBuilder WithYarnPackageInstallation(this ApplicationModel.IResourceBuilder resource, string[]? args = null) { throw null; } } } \ No newline at end of file From 5ec4ea7c3a93814f43839370ee19e35756aa963d Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Mon, 23 Jun 2025 01:39:23 +0000 Subject: [PATCH 4/5] Cleaning up some code to meet perferred style --- .../NodeJSHostingExtensions.cs | 27 +++---------- .../PackageInstallationTests.cs | 38 +++++++++++-------- 2 files changed, 28 insertions(+), 37 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions/NodeJSHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions/NodeJSHostingExtensions.cs index fdfcb290..91d50359 100644 --- a/src/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions/NodeJSHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions/NodeJSHostingExtensions.cs @@ -120,14 +120,9 @@ public static IResourceBuilder WithNpmPackageInstallation(this var installerName = $"{resource.Resource.Name}-npm-install"; var installer = new NpmInstallerResource(installerName, resource.Resource.WorkingDirectory); - var baseArgs = new List { useCI ? "ci" : "install" }; - if (args != null) - { - baseArgs.AddRange(args); - } - + args ??= []; var installerBuilder = resource.ApplicationBuilder.AddResource(installer) - .WithArgs(baseArgs.ToArray()) + .WithArgs([useCI ? "ci" : "install", .. args]) .WithParentRelationship(resource.Resource) .ExcludeFromManifest(); @@ -152,14 +147,9 @@ public static IResourceBuilder WithYarnPackageInstallation(this var installerName = $"{resource.Resource.Name}-yarn-install"; var installer = new YarnInstallerResource(installerName, resource.Resource.WorkingDirectory); - var baseArgs = new List { "install" }; - if (args != null) - { - baseArgs.AddRange(args); - } - + args ??= []; var installerBuilder = resource.ApplicationBuilder.AddResource(installer) - .WithArgs(baseArgs.ToArray()) + .WithArgs(["install", .. args]) .WithParentRelationship(resource.Resource) .ExcludeFromManifest(); @@ -184,14 +174,9 @@ public static IResourceBuilder WithPnpmPackageInstallation(this var installerName = $"{resource.Resource.Name}-pnpm-install"; var installer = new PnpmInstallerResource(installerName, resource.Resource.WorkingDirectory); - var baseArgs = new List { "install" }; - if (args != null) - { - baseArgs.AddRange(args); - } - + args ??= []; var installerBuilder = resource.ApplicationBuilder.AddResource(installer) - .WithArgs(baseArgs.ToArray()) + .WithArgs(["install", .. args]) .WithParentRelationship(resource.Resource) .ExcludeFromManifest(); diff --git a/tests/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.Tests/PackageInstallationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.Tests/PackageInstallationTests.cs index 1c6f4d73..2c7f4eb5 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.Tests/PackageInstallationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.Tests/PackageInstallationTests.cs @@ -1,6 +1,4 @@ using Aspire.Hosting; -using Aspire.Hosting.ApplicationModel; -using Grpc.Core; namespace CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.Tests; @@ -95,16 +93,20 @@ public async Task WithNpmPackageInstallation_CanAcceptAdditionalArgs() // Verify install command with additional args var installArgs = await installResource.GetArgumentValuesAsync(); - Assert.Equal(2, installArgs.Count); - Assert.Equal("install", installArgs[0]); - Assert.Equal("--legacy-peer-deps", installArgs[1]); + Assert.Collection( + installArgs, + arg => Assert.Equal("install", arg), + arg => Assert.Equal("--legacy-peer-deps", arg) + ); // Verify ci command with additional args var ciArgs = await ciResource.GetArgumentValuesAsync(); - Assert.Equal(3, ciArgs.Count); - Assert.Equal("ci", ciArgs[0]); - Assert.Equal("--verbose", ciArgs[1]); - Assert.Equal("--no-optional", ciArgs[2]); + Assert.Collection( + ciArgs, + arg => Assert.Equal("ci", arg), + arg => Assert.Equal("--verbose", arg), + arg => Assert.Equal("--no-optional", arg) + ); } [Fact] @@ -124,10 +126,12 @@ public async Task WithYarnPackageInstallation_CanAcceptAdditionalArgs() Assert.Equal("test-yarn-app-yarn-install", installerResource.Name); var args = await installerResource.GetArgumentValuesAsync(); - Assert.Equal(3, args.Count); - Assert.Equal("install", args[0]); - Assert.Equal("--frozen-lockfile", args[1]); - Assert.Equal("--verbose", args[2]); + Assert.Collection( + args, + arg => Assert.Equal("install", arg), + arg => Assert.Equal("--frozen-lockfile", arg), + arg => Assert.Equal("--verbose", arg) + ); } [Fact] @@ -147,8 +151,10 @@ public async Task WithPnpmPackageInstallation_CanAcceptAdditionalArgs() Assert.Equal("test-pnpm-app-pnpm-install", installerResource.Name); var args = await installerResource.GetArgumentValuesAsync(); - Assert.Equal(2, args.Count); - Assert.Equal("install", args[0]); - Assert.Equal("--frozen-lockfile", args[1]); + Assert.Collection( + args, + arg => Assert.Equal("install", arg), + arg => Assert.Equal("--frozen-lockfile", arg) + ); } } \ No newline at end of file From f5f4b54b84e309cb9a3086bc357cb0fdcfa61cf8 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Mon, 23 Jun 2025 01:51:55 +0000 Subject: [PATCH 5/5] Moving to standard wait approach --- .../Program.cs | 14 ++++++-------- .../AppHostTests.cs | 3 +-- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/examples/nodejs-ext/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.AppHost/Program.cs b/examples/nodejs-ext/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.AppHost/Program.cs index ddc0ba73..6170fd87 100644 --- a/examples/nodejs-ext/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.AppHost/Program.cs +++ b/examples/nodejs-ext/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.AppHost/Program.cs @@ -1,17 +1,15 @@ var builder = DistributedApplication.CreateBuilder(args); builder.AddViteApp("vite-demo") - .WithNpmPackageInstallation(); + .WithNpmPackageInstallation() + .WithHttpHealthCheck(); builder.AddViteApp("yarn-demo", packageManager: "yarn") - .WithYarnPackageInstallation(); + .WithYarnPackageInstallation() + .WithHttpHealthCheck(); builder.AddViteApp("pnpm-demo", packageManager: "pnpm") - .WithPnpmPackageInstallation(); - -// Example of using custom args - useful for legacy packages -builder.AddNpmApp("npm-with-flags", "../vite-demo") - .WithNpmPackageInstallation(useCI: false, args: ["--legacy-peer-deps"]) - .WithHttpEndpoint(env: "PORT"); + .WithPnpmPackageInstallation() + .WithHttpHealthCheck(); builder.Build().Run(); diff --git a/tests/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.Tests/AppHostTests.cs index 939e8219..bc07605b 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.Tests/AppHostTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.Tests/AppHostTests.cs @@ -2,7 +2,6 @@ namespace CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.Tests; -#pragma warning disable CTASPIRE001 public class AppHostTests(AspireIntegrationTestFixture fixture) : IClassFixture> { [Theory] @@ -13,7 +12,7 @@ public async Task ResourceStartsAndRespondsOk(string appName) { var httpClient = fixture.CreateHttpClient(appName); - await fixture.App.WaitForTextAsync("VITE", appName).WaitAsync(TimeSpan.FromSeconds(30)); + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(appName).WaitAsync(TimeSpan.FromMinutes(1)); var response = await httpClient.GetAsync("/");