Skip to content

Commit 21a6826

Browse files
Copilotaaronpowell
andauthored
Add support for passing additional arguments to package installation methods (#739)
* Initial plan for issue * Add args parameter to package installation methods Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> * Add documentation and example for new args functionality Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> * Cleaning up some code to meet perferred style * Moving to standard wait approach --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> Co-authored-by: Aaron Powell <me@aaron-powell.com>
1 parent 5b92724 commit 21a6826

File tree

6 files changed

+132
-16
lines changed

6 files changed

+132
-16
lines changed
Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
var builder = DistributedApplication.CreateBuilder(args);
22

33
builder.AddViteApp("vite-demo")
4-
.WithNpmPackageInstallation();
4+
.WithNpmPackageInstallation()
5+
.WithHttpHealthCheck();
56

67
builder.AddViteApp("yarn-demo", packageManager: "yarn")
7-
.WithYarnPackageInstallation();
8+
.WithYarnPackageInstallation()
9+
.WithHttpHealthCheck();
810

911
builder.AddViteApp("pnpm-demo", packageManager: "pnpm")
10-
.WithPnpmPackageInstallation();
12+
.WithPnpmPackageInstallation()
13+
.WithHttpHealthCheck();
1114

1215
builder.Build().Run();

src/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions/NodeJSHostingExtensions.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,17 +110,19 @@ public static IResourceBuilder<NodeAppResource> AddPnpmApp(this IDistributedAppl
110110
/// </summary>
111111
/// <param name="resource">The Node.js app resource.</param>
112112
/// <param name="useCI">When true use <code>npm ci</code> otherwise use <code>npm install</code> when installing packages.</param>
113+
/// <param name="args">Additional arguments to pass to the npm command.</param>
113114
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
114-
public static IResourceBuilder<NodeAppResource> WithNpmPackageInstallation(this IResourceBuilder<NodeAppResource> resource, bool useCI = false)
115+
public static IResourceBuilder<NodeAppResource> WithNpmPackageInstallation(this IResourceBuilder<NodeAppResource> resource, bool useCI = false, string[]? args = null)
115116
{
116117
// Only install packages during development, not in publish mode
117118
if (!resource.ApplicationBuilder.ExecutionContext.IsPublishMode)
118119
{
119120
var installerName = $"{resource.Resource.Name}-npm-install";
120121
var installer = new NpmInstallerResource(installerName, resource.Resource.WorkingDirectory);
121122

123+
args ??= [];
122124
var installerBuilder = resource.ApplicationBuilder.AddResource(installer)
123-
.WithArgs([useCI ? "ci" : "install"])
125+
.WithArgs([useCI ? "ci" : "install", .. args])
124126
.WithParentRelationship(resource.Resource)
125127
.ExcludeFromManifest();
126128

@@ -135,17 +137,19 @@ public static IResourceBuilder<NodeAppResource> WithNpmPackageInstallation(this
135137
/// Ensures the Node.js packages are installed before the application starts using yarn as the package manager.
136138
/// </summary>
137139
/// <param name="resource">The Node.js app resource.</param>
140+
/// <param name="args">Additional arguments to pass to the yarn command.</param>
138141
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
139-
public static IResourceBuilder<NodeAppResource> WithYarnPackageInstallation(this IResourceBuilder<NodeAppResource> resource)
142+
public static IResourceBuilder<NodeAppResource> WithYarnPackageInstallation(this IResourceBuilder<NodeAppResource> resource, string[]? args = null)
140143
{
141144
// Only install packages during development, not in publish mode
142145
if (!resource.ApplicationBuilder.ExecutionContext.IsPublishMode)
143146
{
144147
var installerName = $"{resource.Resource.Name}-yarn-install";
145148
var installer = new YarnInstallerResource(installerName, resource.Resource.WorkingDirectory);
146149

150+
args ??= [];
147151
var installerBuilder = resource.ApplicationBuilder.AddResource(installer)
148-
.WithArgs(["install"])
152+
.WithArgs(["install", .. args])
149153
.WithParentRelationship(resource.Resource)
150154
.ExcludeFromManifest();
151155

@@ -160,17 +164,19 @@ public static IResourceBuilder<NodeAppResource> WithYarnPackageInstallation(this
160164
/// Ensures the Node.js packages are installed before the application starts using pnpm as the package manager.
161165
/// </summary>
162166
/// <param name="resource">The Node.js app resource.</param>
167+
/// <param name="args">Additional arguments to pass to the pnpm command.</param>
163168
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
164-
public static IResourceBuilder<NodeAppResource> WithPnpmPackageInstallation(this IResourceBuilder<NodeAppResource> resource)
169+
public static IResourceBuilder<NodeAppResource> WithPnpmPackageInstallation(this IResourceBuilder<NodeAppResource> resource, string[]? args = null)
165170
{
166171
// Only install packages during development, not in publish mode
167172
if (!resource.ApplicationBuilder.ExecutionContext.IsPublishMode)
168173
{
169174
var installerName = $"{resource.Resource.Name}-pnpm-install";
170175
var installer = new PnpmInstallerResource(installerName, resource.Resource.WorkingDirectory);
171176

177+
args ??= [];
172178
var installerBuilder = resource.ApplicationBuilder.AddResource(installer)
173-
.WithArgs(["install"])
179+
.WithArgs(["install", .. args])
174180
.WithParentRelationship(resource.Resource)
175181
.ExcludeFromManifest();
176182

src/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,27 @@ builder.AddPnpmApp("pnpm-demo")
2424
.WithExternalHttpEndpoints();
2525
```
2626

27+
### Package installation with custom flags
28+
29+
You can pass additional flags to package managers during installation:
30+
31+
```csharp
32+
// npm with legacy peer deps support
33+
builder.AddNpmApp("npm-app", "./path/to/app")
34+
.WithNpmPackageInstallation(useCI: false, args: ["--legacy-peer-deps"])
35+
.WithExternalHttpEndpoints();
36+
37+
// yarn with frozen lockfile
38+
builder.AddYarnApp("yarn-app", "./path/to/app")
39+
.WithYarnPackageInstallation(args: ["--frozen-lockfile", "--verbose"])
40+
.WithExternalHttpEndpoints();
41+
42+
// pnpm with frozen lockfile
43+
builder.AddPnpmApp("pnpm-app", "./path/to/app")
44+
.WithPnpmPackageInstallation(args: ["--frozen-lockfile"])
45+
.WithExternalHttpEndpoints();
46+
```
47+
2748
## Additional Information
2849

2950
https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-nodejs-extensions

src/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions/api/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ public static partial class NodeJSHostingExtensions
1616

1717
public static ApplicationModel.IResourceBuilder<NodeAppResource> AddYarnApp(this IDistributedApplicationBuilder builder, string name, string workingDirectory, string scriptName = "start", string[]? args = null) { throw null; }
1818

19-
public static ApplicationModel.IResourceBuilder<NodeAppResource> WithNpmPackageInstallation(this ApplicationModel.IResourceBuilder<NodeAppResource> resource, bool useCI = false) { throw null; }
19+
public static ApplicationModel.IResourceBuilder<NodeAppResource> WithNpmPackageInstallation(this ApplicationModel.IResourceBuilder<NodeAppResource> resource, bool useCI = false, string[]? args = null) { throw null; }
2020

21-
public static ApplicationModel.IResourceBuilder<NodeAppResource> WithPnpmPackageInstallation(this ApplicationModel.IResourceBuilder<NodeAppResource> resource) { throw null; }
21+
public static ApplicationModel.IResourceBuilder<NodeAppResource> WithPnpmPackageInstallation(this ApplicationModel.IResourceBuilder<NodeAppResource> resource, string[]? args = null) { throw null; }
2222

23-
public static ApplicationModel.IResourceBuilder<NodeAppResource> WithYarnPackageInstallation(this ApplicationModel.IResourceBuilder<NodeAppResource> resource) { throw null; }
23+
public static ApplicationModel.IResourceBuilder<NodeAppResource> WithYarnPackageInstallation(this ApplicationModel.IResourceBuilder<NodeAppResource> resource, string[]? args = null) { throw null; }
2424
}
2525
}

tests/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.Tests/AppHostTests.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.Tests;
44

5-
#pragma warning disable CTASPIRE001
65
public class AppHostTests(AspireIntegrationTestFixture<Projects.CommunityToolkit_Aspire_Hosting_NodeJS_Extensions_AppHost> fixture) : IClassFixture<AspireIntegrationTestFixture<Projects.CommunityToolkit_Aspire_Hosting_NodeJS_Extensions_AppHost>>
76
{
87
[Theory]
@@ -13,7 +12,7 @@ public async Task ResourceStartsAndRespondsOk(string appName)
1312
{
1413
var httpClient = fixture.CreateHttpClient(appName);
1514

16-
await fixture.App.WaitForTextAsync("VITE", appName).WaitAsync(TimeSpan.FromSeconds(30));
15+
await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(appName).WaitAsync(TimeSpan.FromMinutes(1));
1716

1817
var response = await httpClient.GetAsync("/");
1918

tests/CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.Tests/PackageInstallationTests.cs

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
using Aspire.Hosting;
2-
using Aspire.Hosting.ApplicationModel;
3-
using Grpc.Core;
42

53
namespace CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.Tests;
64

@@ -70,4 +68,93 @@ public void WithNpmPackageInstallation_ExcludedFromPublishMode()
7068
// Verify no wait annotations were added
7169
Assert.False(nodeResource.TryGetAnnotationsOfType<WaitAnnotation>(out _));
7270
}
71+
72+
[Fact]
73+
public async Task WithNpmPackageInstallation_CanAcceptAdditionalArgs()
74+
{
75+
var builder = DistributedApplication.CreateBuilder();
76+
77+
var nodeApp = builder.AddNpmApp("test-app", "./test-app");
78+
var nodeAppWithArgs = builder.AddNpmApp("test-app-args", "./test-app-args");
79+
80+
// Test npm install with additional args
81+
nodeApp.WithNpmPackageInstallation(useCI: false, args: ["--legacy-peer-deps"]);
82+
nodeAppWithArgs.WithNpmPackageInstallation(useCI: true, args: ["--verbose", "--no-optional"]);
83+
84+
using var app = builder.Build();
85+
86+
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
87+
var installerResources = appModel.Resources.OfType<NpmInstallerResource>().ToList();
88+
89+
Assert.Equal(2, installerResources.Count);
90+
91+
var installResource = installerResources.Single(r => r.Name == "test-app-npm-install");
92+
var ciResource = installerResources.Single(r => r.Name == "test-app-args-npm-install");
93+
94+
// Verify install command with additional args
95+
var installArgs = await installResource.GetArgumentValuesAsync();
96+
Assert.Collection(
97+
installArgs,
98+
arg => Assert.Equal("install", arg),
99+
arg => Assert.Equal("--legacy-peer-deps", arg)
100+
);
101+
102+
// Verify ci command with additional args
103+
var ciArgs = await ciResource.GetArgumentValuesAsync();
104+
Assert.Collection(
105+
ciArgs,
106+
arg => Assert.Equal("ci", arg),
107+
arg => Assert.Equal("--verbose", arg),
108+
arg => Assert.Equal("--no-optional", arg)
109+
);
110+
}
111+
112+
[Fact]
113+
public async Task WithYarnPackageInstallation_CanAcceptAdditionalArgs()
114+
{
115+
var builder = DistributedApplication.CreateBuilder();
116+
117+
var nodeApp = builder.AddYarnApp("test-yarn-app", "./test-yarn-app");
118+
nodeApp.WithYarnPackageInstallation(args: ["--frozen-lockfile", "--verbose"]);
119+
120+
using var app = builder.Build();
121+
122+
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
123+
var installerResources = appModel.Resources.OfType<YarnInstallerResource>().ToList();
124+
125+
var installerResource = Assert.Single(installerResources);
126+
Assert.Equal("test-yarn-app-yarn-install", installerResource.Name);
127+
128+
var args = await installerResource.GetArgumentValuesAsync();
129+
Assert.Collection(
130+
args,
131+
arg => Assert.Equal("install", arg),
132+
arg => Assert.Equal("--frozen-lockfile", arg),
133+
arg => Assert.Equal("--verbose", arg)
134+
);
135+
}
136+
137+
[Fact]
138+
public async Task WithPnpmPackageInstallation_CanAcceptAdditionalArgs()
139+
{
140+
var builder = DistributedApplication.CreateBuilder();
141+
142+
var nodeApp = builder.AddPnpmApp("test-pnpm-app", "./test-pnpm-app");
143+
nodeApp.WithPnpmPackageInstallation(args: ["--frozen-lockfile"]);
144+
145+
using var app = builder.Build();
146+
147+
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
148+
var installerResources = appModel.Resources.OfType<PnpmInstallerResource>().ToList();
149+
150+
var installerResource = Assert.Single(installerResources);
151+
Assert.Equal("test-pnpm-app-pnpm-install", installerResource.Name);
152+
153+
var args = await installerResource.GetArgumentValuesAsync();
154+
Assert.Collection(
155+
args,
156+
arg => Assert.Equal("install", arg),
157+
arg => Assert.Equal("--frozen-lockfile", arg)
158+
);
159+
}
73160
}

0 commit comments

Comments
 (0)