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..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,12 +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(); + .WithPnpmPackageInstallation() + .WithHttpHealthCheck(); builder.Build().Run(); diff --git a/src/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions/NodeJSHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions/NodeJSHostingExtensions.cs index 61f0cb9a..91d50359 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,9 @@ public static IResourceBuilder WithNpmPackageInstallation(this var installerName = $"{resource.Resource.Name}-npm-install"; var installer = new NpmInstallerResource(installerName, resource.Resource.WorkingDirectory); + args ??= []; var installerBuilder = resource.ApplicationBuilder.AddResource(installer) - .WithArgs([useCI ? "ci" : "install"]) + .WithArgs([useCI ? "ci" : "install", .. args]) .WithParentRelationship(resource.Resource) .ExcludeFromManifest(); @@ -135,8 +137,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 +147,9 @@ public static IResourceBuilder WithYarnPackageInstallation(this var installerName = $"{resource.Resource.Name}-yarn-install"; var installer = new YarnInstallerResource(installerName, resource.Resource.WorkingDirectory); + args ??= []; var installerBuilder = resource.ApplicationBuilder.AddResource(installer) - .WithArgs(["install"]) + .WithArgs(["install", .. args]) .WithParentRelationship(resource.Resource) .ExcludeFromManifest(); @@ -160,8 +164,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 +174,9 @@ public static IResourceBuilder WithPnpmPackageInstallation(this var installerName = $"{resource.Resource.Name}-pnpm-install"; var installer = new PnpmInstallerResource(installerName, resource.Resource.WorkingDirectory); + args ??= []; var installerBuilder = resource.ApplicationBuilder.AddResource(installer) - .WithArgs(["install"]) + .WithArgs(["install", .. args]) .WithParentRelationship(resource.Resource) .ExcludeFromManifest(); 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 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("/"); diff --git a/tests/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.Tests/PackageInstallationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.Tests/PackageInstallationTests.cs index b0a96cac..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; @@ -70,4 +68,93 @@ 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.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.Collection( + ciArgs, + arg => Assert.Equal("ci", arg), + arg => Assert.Equal("--verbose", arg), + arg => Assert.Equal("--no-optional", arg) + ); + } + + [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.Collection( + args, + arg => Assert.Equal("install", arg), + arg => Assert.Equal("--frozen-lockfile", arg), + arg => Assert.Equal("--verbose", arg) + ); + } + + [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.Collection( + args, + arg => Assert.Equal("install", arg), + arg => Assert.Equal("--frozen-lockfile", arg) + ); + } } \ No newline at end of file