Skip to content

Commit 0b845e5

Browse files
Copilotaaronpowell
andcommitted
Add integration tests and documentation for resource-based package installers
Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com>
1 parent 507380a commit 0b845e5

File tree

2 files changed

+201
-0
lines changed

2 files changed

+201
-0
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Node.js Package Installer Refactoring
2+
3+
This refactoring transforms the Node.js package installers from lifecycle hooks to ExecutableResource-based resources, addressing issue #732.
4+
5+
## What Changed
6+
7+
### Before (Lifecycle Hook Approach)
8+
- Package installation was handled by lifecycle hooks during `BeforeStartAsync`
9+
- No visibility into installation progress in the dashboard
10+
- Limited logging capabilities
11+
- Process management handled manually via `Process.Start`
12+
13+
### After (Resource-Based Approach)
14+
- Package installers are now proper `ExecutableResource` instances
15+
- They appear as separate resources in the Aspire dashboard
16+
- Full console output visibility and logging
17+
- DCP (Distributed Application Control Plane) handles process management
18+
- Parent-child relationships ensure proper startup ordering
19+
20+
## New Resource Classes
21+
22+
### NpmInstallerResource
23+
```csharp
24+
var installer = new NpmInstallerResource("npm-installer", "/path/to/project", useCI: true);
25+
// Supports both 'npm install' and 'npm ci' commands
26+
```
27+
28+
### YarnInstallerResource
29+
```csharp
30+
var installer = new YarnInstallerResource("yarn-installer", "/path/to/project");
31+
// Executes 'yarn install' command
32+
```
33+
34+
### PnpmInstallerResource
35+
```csharp
36+
var installer = new PnpmInstallerResource("pnpm-installer", "/path/to/project");
37+
// Executes 'pnpm install' command
38+
```
39+
40+
## Usage Examples
41+
42+
### Basic Usage (No API Changes)
43+
```csharp
44+
var builder = DistributedApplication.CreateBuilder();
45+
46+
// API remains the same - behavior is now resource-based
47+
var viteApp = builder.AddViteApp("frontend", "./frontend")
48+
.WithNpmPackageInstallation(useCI: true);
49+
50+
var backendApp = builder.AddYarnApp("backend", "./backend")
51+
.WithYarnPackageInstallation();
52+
```
53+
54+
### What Happens Under the Hood
55+
```csharp
56+
// This now creates:
57+
// 1. NodeAppResource named "frontend"
58+
// 2. NpmInstallerResource named "frontend-npm-install" (child of frontend)
59+
// 3. WaitAnnotation on frontend to wait for installer completion
60+
// 4. ResourceRelationshipAnnotation linking installer to parent
61+
```
62+
63+
## Benefits
64+
65+
### Dashboard Visibility
66+
- Installer resources appear as separate items in the Aspire dashboard
67+
- Real-time console output from package installation
68+
- Clear status indication (starting, running, completed, failed)
69+
- Ability to re-run installations if needed
70+
71+
### Better Resource Management
72+
- DCP handles process lifecycle instead of manual `Process.Start`
73+
- Proper resource cleanup and error handling
74+
- Integration with Aspire's logging and monitoring systems
75+
76+
### Improved Startup Ordering
77+
- Parent resources automatically wait for installer completion
78+
- Failed installations prevent app startup (fail-fast behavior)
79+
- Clear dependency visualization in the dashboard
80+
81+
### Development vs Production
82+
- Installers only run during development (excluded from publish mode)
83+
- No overhead in production deployments
84+
- Maintains backward compatibility
85+
86+
## Migration Guide
87+
88+
### For Users
89+
No changes required! The existing APIs (`WithNpmPackageInstallation`, `WithYarnPackageInstallation`, `WithPnpmPackageInstallation`) work exactly the same.
90+
91+
### For Contributors
92+
The lifecycle hook classes are marked as `[Obsolete]` but remain functional for backward compatibility:
93+
- `NpmPackageInstallerLifecycleHook`
94+
- `YarnPackageInstallerLifecycleHook`
95+
- `PnpmPackageInstallerLifecycleHook`
96+
- `NodePackageInstaller`
97+
98+
These will be removed in a future version once all usage has migrated to the resource-based approach.
99+
100+
## Testing
101+
102+
Comprehensive test coverage includes:
103+
- Unit tests for installer resource properties and command generation
104+
- Integration tests for parent-child relationships
105+
- Cross-platform compatibility (Windows vs Unix commands)
106+
- Publish mode exclusion verification
107+
- Wait annotation and resource relationship validation
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
using Aspire.Hosting.ApplicationModel;
2+
3+
namespace CommunityToolkit.Aspire.Hosting.NodeJS.Extensions.Tests;
4+
5+
/// <summary>
6+
/// Integration test that demonstrates the new resource-based package installer architecture.
7+
/// This shows how installer resources appear as separate resources in the application model.
8+
/// </summary>
9+
public class IntegrationTests
10+
{
11+
[Fact]
12+
public void ResourceBasedPackageInstallersAppearInApplicationModel()
13+
{
14+
var builder = DistributedApplication.CreateBuilder();
15+
16+
// Add multiple Node.js apps with different package managers
17+
var viteApp = builder.AddViteApp("vite-app", "./frontend")
18+
.WithNpmPackageInstallation(useCI: true);
19+
20+
var yarnApp = builder.AddYarnApp("yarn-app", "./backend")
21+
.WithYarnPackageInstallation();
22+
23+
var pnpmApp = builder.AddPnpmApp("pnpm-app", "./admin")
24+
.WithPnpmPackageInstallation();
25+
26+
using var app = builder.Build();
27+
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
28+
29+
// Verify all Node.js app resources are present
30+
var nodeResources = appModel.Resources.OfType<NodeAppResource>().ToList();
31+
Assert.Equal(3, nodeResources.Count);
32+
33+
// Verify all installer resources are present as separate resources
34+
var npmInstallers = appModel.Resources.OfType<NpmInstallerResource>().ToList();
35+
var yarnInstallers = appModel.Resources.OfType<YarnInstallerResource>().ToList();
36+
var pnpmInstallers = appModel.Resources.OfType<PnpmInstallerResource>().ToList();
37+
38+
Assert.Single(npmInstallers);
39+
Assert.Single(yarnInstallers);
40+
Assert.Single(pnpmInstallers);
41+
42+
// Verify installer resources have expected names (would appear on dashboard)
43+
Assert.Equal("vite-app-npm-install", npmInstallers[0].Name);
44+
Assert.Equal("yarn-app-yarn-install", yarnInstallers[0].Name);
45+
Assert.Equal("pnpm-app-pnpm-install", pnpmInstallers[0].Name);
46+
47+
// Verify parent-child relationships
48+
foreach (var installer in npmInstallers.Cast<IResource>()
49+
.Concat(yarnInstallers.Cast<IResource>())
50+
.Concat(pnpmInstallers.Cast<IResource>()))
51+
{
52+
Assert.True(installer.TryGetAnnotationsOfType<ResourceRelationshipAnnotation>(out var relationships));
53+
Assert.Single(relationships);
54+
Assert.Equal("Parent", relationships.First().Relationship);
55+
}
56+
57+
// Verify all Node.js apps wait for their installers
58+
foreach (var nodeApp in nodeResources)
59+
{
60+
Assert.True(nodeApp.TryGetAnnotationsOfType<WaitAnnotation>(out var waitAnnotations));
61+
Assert.Single(waitAnnotations);
62+
63+
var waitedResource = waitAnnotations.First().Resource;
64+
Assert.True(waitedResource is NpmInstallerResource ||
65+
waitedResource is YarnInstallerResource ||
66+
waitedResource is PnpmInstallerResource);
67+
}
68+
}
69+
70+
[Fact]
71+
public void InstallerResourcesHaveCorrectExecutableConfiguration()
72+
{
73+
var builder = DistributedApplication.CreateBuilder();
74+
75+
var nodeApp = builder.AddNpmApp("test-app", "./test")
76+
.WithNpmPackageInstallation(useCI: true);
77+
78+
using var app = builder.Build();
79+
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
80+
81+
var installer = Assert.Single(appModel.Resources.OfType<NpmInstallerResource>());
82+
83+
// Verify it's configured as an ExecutableResource
84+
Assert.IsAssignableFrom<ExecutableResource>(installer);
85+
86+
// Verify working directory matches parent
87+
var parentApp = Assert.Single(appModel.Resources.OfType<NodeAppResource>());
88+
Assert.Equal(parentApp.WorkingDirectory, installer.WorkingDirectory);
89+
90+
// Verify command arguments are configured
91+
Assert.True(installer.TryGetAnnotationsOfType<CommandLineArgsCallbackAnnotation>(out var argsAnnotations) ||
92+
installer.TryGetAnnotationsOfType<CommandLineArgsAnnotation>(out var directArgs));
93+
}
94+
}

0 commit comments

Comments
 (0)