diff --git a/Aspire.slnx b/Aspire.slnx
index d2ce1f43147..2dab93a8f6a 100644
--- a/Aspire.slnx
+++ b/Aspire.slnx
@@ -106,6 +106,11 @@
     
     
   
+  
+    
+    
+    
+  
   
     
     
@@ -176,8 +181,8 @@
     
   
   
-    
     
+    
     
   
   
@@ -199,11 +204,6 @@
   
     
   
-  
-    
-    
-    
-  
   
     
     
@@ -234,8 +234,8 @@
   
   
     
-    
     
+    
   
   
     
diff --git a/playground/AspireWithJavaScript/AspireJavaScript.AppHost/AppHost.cs b/playground/AspireWithJavaScript/AspireJavaScript.AppHost/AppHost.cs
index 86b7914f5b1..15c89c54b9e 100644
--- a/playground/AspireWithJavaScript/AspireJavaScript.AppHost/AppHost.cs
+++ b/playground/AspireWithJavaScript/AspireJavaScript.AppHost/AppHost.cs
@@ -1,9 +1,10 @@
-var builder = DistributedApplication.CreateBuilder(args);
+var builder = DistributedApplication.CreateBuilder(args);
 
 var weatherApi = builder.AddProject("weatherapi")
     .WithExternalHttpEndpoints();
 
 builder.AddNpmApp("angular", "../AspireJavaScript.Angular")
+    .WithNpmPackageManager()
     .WithReference(weatherApi)
     .WaitFor(weatherApi)
     .WithHttpEndpoint(env: "PORT")
@@ -11,6 +12,7 @@
     .PublishAsDockerFile();
 
 builder.AddNpmApp("react", "../AspireJavaScript.React")
+    .WithNpmPackageManager()
     .WithReference(weatherApi)
     .WaitFor(weatherApi)
     .WithEnvironment("BROWSER", "none") // Disable opening browser on npm start
@@ -19,17 +21,17 @@
     .PublishAsDockerFile();
 
 builder.AddNpmApp("vue", "../AspireJavaScript.Vue")
+    .WithNpmPackageManager()
     .WithReference(weatherApi)
     .WaitFor(weatherApi)
     .WithHttpEndpoint(env: "PORT")
     .WithExternalHttpEndpoints()
     .PublishAsDockerFile();
 
-builder.AddNpmApp("reactvite", "../AspireJavaScript.Vite")
+builder.AddViteApp("reactvite", "../AspireJavaScript.Vite")
+    .WithNpmPackageManager()
     .WithReference(weatherApi)
     .WithEnvironment("BROWSER", "none")
-    .WithHttpEndpoint(env: "VITE_PORT")
-    .WithExternalHttpEndpoints()
-    .PublishAsDockerFile();
+    .WithExternalHttpEndpoints();
 
 builder.Build().Run();
diff --git a/playground/AspireWithJavaScript/AspireJavaScript.AppHost/AspireJavaScript.AppHost.csproj b/playground/AspireWithJavaScript/AspireJavaScript.AppHost/AspireJavaScript.AppHost.csproj
index aef5911e35f..262a263f6d4 100644
--- a/playground/AspireWithJavaScript/AspireJavaScript.AppHost/AspireJavaScript.AppHost.csproj
+++ b/playground/AspireWithJavaScript/AspireJavaScript.AppHost/AspireJavaScript.AppHost.csproj
@@ -18,22 +18,4 @@
     
   
 
-  
-    
-      
-    
-    
-    
-  
-
-  
-    
-      
-    
-
-    
-    
-    
-  
-
 
diff --git a/playground/AspireWithJavaScript/AspireJavaScript.AppHost/aspire-manifest.json b/playground/AspireWithJavaScript/AspireJavaScript.AppHost/aspire-manifest.json
index 4b9f100e483..8e2b284d216 100644
--- a/playground/AspireWithJavaScript/AspireJavaScript.AppHost/aspire-manifest.json
+++ b/playground/AspireWithJavaScript/AspireJavaScript.AppHost/aspire-manifest.json
@@ -27,12 +27,16 @@
       }
     },
     "angular": {
-      "type": "dockerfile.v0",
-      "path": "../AspireJavaScript.Angular/Dockerfile",
-      "context": "../AspireJavaScript.Angular",
+      "type": "container.v1",
+      "build": {
+        "context": "../AspireJavaScript.Angular",
+        "dockerfile": "../AspireJavaScript.Angular/Dockerfile"
+      },
       "env": {
         "NODE_ENV": "development",
+        "WEATHERAPI_HTTP": "{weatherapi.bindings.http.url}",
         "services__weatherapi__http__0": "{weatherapi.bindings.http.url}",
+        "WEATHERAPI_HTTPS": "{weatherapi.bindings.https.url}",
         "services__weatherapi__https__0": "{weatherapi.bindings.https.url}",
         "PORT": "{angular.bindings.http.targetPort}"
       },
@@ -47,12 +51,16 @@
       }
     },
     "react": {
-      "type": "dockerfile.v0",
-      "path": "../AspireJavaScript.React/Dockerfile",
-      "context": "../AspireJavaScript.React",
+      "type": "container.v1",
+      "build": {
+        "context": "../AspireJavaScript.React",
+        "dockerfile": "../AspireJavaScript.React/Dockerfile"
+      },
       "env": {
         "NODE_ENV": "development",
+        "WEATHERAPI_HTTP": "{weatherapi.bindings.http.url}",
         "services__weatherapi__http__0": "{weatherapi.bindings.http.url}",
+        "WEATHERAPI_HTTPS": "{weatherapi.bindings.https.url}",
         "services__weatherapi__https__0": "{weatherapi.bindings.https.url}",
         "BROWSER": "none",
         "PORT": "{react.bindings.http.targetPort}"
@@ -68,12 +76,16 @@
       }
     },
     "vue": {
-      "type": "dockerfile.v0",
-      "path": "../AspireJavaScript.Vue/Dockerfile",
-      "context": "../AspireJavaScript.Vue",
+      "type": "container.v1",
+      "build": {
+        "context": "../AspireJavaScript.Vue",
+        "dockerfile": "../AspireJavaScript.Vue/Dockerfile"
+      },
       "env": {
         "NODE_ENV": "development",
+        "WEATHERAPI_HTTP": "{weatherapi.bindings.http.url}",
         "services__weatherapi__http__0": "{weatherapi.bindings.http.url}",
+        "WEATHERAPI_HTTPS": "{weatherapi.bindings.https.url}",
         "services__weatherapi__https__0": "{weatherapi.bindings.https.url}",
         "PORT": "{vue.bindings.http.targetPort}"
       },
@@ -86,6 +98,31 @@
           "external": true
         }
       }
+    },
+    "reactvite": {
+      "type": "container.v1",
+      "build": {
+        "context": "../AspireJavaScript.Vite",
+        "dockerfile": "reactvite.Dockerfile"
+      },
+      "env": {
+        "NODE_ENV": "development",
+        "PORT": "{reactvite.bindings.http.targetPort}",
+        "WEATHERAPI_HTTP": "{weatherapi.bindings.http.url}",
+        "services__weatherapi__http__0": "{weatherapi.bindings.http.url}",
+        "WEATHERAPI_HTTPS": "{weatherapi.bindings.https.url}",
+        "services__weatherapi__https__0": "{weatherapi.bindings.https.url}",
+        "BROWSER": "none"
+      },
+      "bindings": {
+        "http": {
+          "scheme": "http",
+          "protocol": "tcp",
+          "transport": "http",
+          "targetPort": 8003,
+          "external": true
+        }
+      }
     }
   }
 }
\ No newline at end of file
diff --git a/playground/AspireWithJavaScript/AspireJavaScript.AppHost/reactvite.Dockerfile b/playground/AspireWithJavaScript/AspireJavaScript.AppHost/reactvite.Dockerfile
new file mode 100644
index 00000000000..3bafae638df
--- /dev/null
+++ b/playground/AspireWithJavaScript/AspireJavaScript.AppHost/reactvite.Dockerfile
@@ -0,0 +1,5 @@
+FROM node:22-slim
+WORKDIR /app
+COPY . .
+RUN npm install
+RUN npm run build
diff --git a/playground/AspireWithJavaScript/AspireJavaScript.Vite/Dockerfile b/playground/AspireWithJavaScript/AspireJavaScript.Vite/Dockerfile
deleted file mode 100644
index 2ac3df971b3..00000000000
--- a/playground/AspireWithJavaScript/AspireJavaScript.Vite/Dockerfile
+++ /dev/null
@@ -1,23 +0,0 @@
-# DisableDockerDetector "Playground/demo application used for testing Aspire features"
-FROM node:20 as build
-
-WORKDIR /app
-
-COPY package.json package.json
-COPY package-lock.json package-lock.json
-
-RUN npm install
-
-COPY . .
-
-RUN npm run build
-
-FROM nginx:alpine
-
-COPY --from=build /app/default.conf.template /etc/nginx/templates/default.conf.template
-COPY --from=build /app/dist /usr/share/nginx/html
-
-# Expose the default nginx port
-EXPOSE 80
-
-CMD ["nginx", "-g", "daemon off;"]
diff --git a/src/Aspire.Hosting.NodeJs/JavaScriptPackageInstallerAnnotation.cs b/src/Aspire.Hosting.NodeJs/JavaScriptPackageInstallerAnnotation.cs
new file mode 100644
index 00000000000..bdeb1ee3053
--- /dev/null
+++ b/src/Aspire.Hosting.NodeJs/JavaScriptPackageInstallerAnnotation.cs
@@ -0,0 +1,17 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+
+namespace Aspire.Hosting.NodeJs;
+
+/// 
+/// Represents an annotation for a JavaScript installer resource.
+/// 
+public sealed class JavaScriptPackageInstallerAnnotation(ExecutableResource installerResource) : IResourceAnnotation
+{
+    /// 
+    /// The instance of the Installer resource used.
+    /// 
+    public ExecutableResource Resource { get; } = installerResource;
+}
\ No newline at end of file
diff --git a/src/Aspire.Hosting.NodeJs/JavaScriptPackageManagerAnnotation.cs b/src/Aspire.Hosting.NodeJs/JavaScriptPackageManagerAnnotation.cs
new file mode 100644
index 00000000000..12d2fae0fa6
--- /dev/null
+++ b/src/Aspire.Hosting.NodeJs/JavaScriptPackageManagerAnnotation.cs
@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+
+namespace Aspire.Hosting.NodeJs;
+
+/// 
+/// Represents the annotation for the JavaScript package manager used in a resource.
+/// 
+/// The name of the JavaScript package manager.
+public sealed class JavaScriptPackageManagerAnnotation(string packageManager) : IResourceAnnotation
+{
+    /// 
+    /// Gets the name of the JavaScript package manager.
+    /// 
+    public string PackageManager { get; } = packageManager;
+
+    /// 
+    /// Gets the command line arguments for the JavaScript package manager's install command.
+    /// 
+    public string[] InstallCommandLineArgs { get; init; } = [];
+
+    /// 
+    /// Gets the command line arguments for the JavaScript package manager's run command.
+    /// 
+    public string[] RunCommandLineArgs { get; init; } = [];
+
+    /// 
+    /// Gets a string value that separates the package manager command line args from the tool's command line args.
+    /// By default, this is "--".
+    /// 
+    public string? CommandSeparator { get; init; } = "--";
+
+    /// 
+    /// Gets the command line arguments for the JavaScript package manager's command that produces assets for distribution.
+    /// 
+    public string[] BuildCommandLineArgs { get; init; } = [];
+}
diff --git a/src/Aspire.Hosting.NodeJs/NodeExtensions.cs b/src/Aspire.Hosting.NodeJs/NodeExtensions.cs
index 4bdd94818c8..dc133ade377 100644
--- a/src/Aspire.Hosting.NodeJs/NodeExtensions.cs
+++ b/src/Aspire.Hosting.NodeJs/NodeExtensions.cs
@@ -1,6 +1,10 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
+
+#pragma warning disable ASPIREDOCKERFILEBUILDER001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+
 using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.NodeJs;
 using Aspire.Hosting.Utils;
 using Microsoft.Extensions.Hosting;
 
@@ -69,7 +73,7 @@ public static IResourceBuilder AddNpmApp(this IDistributedAppli
                       .WithIconName("CodeJsRectangle");
     }
 
-    private static IResourceBuilder WithNodeDefaults(this IResourceBuilder builder) =>
+    private static IResourceBuilder WithNodeDefaults(this IResourceBuilder builder) where TResource : NodeAppResource =>
         builder.WithOtlpExporter()
             .WithEnvironment("NODE_ENV", builder.ApplicationBuilder.Environment.IsDevelopment() ? "development" : "production")
             .WithExecutableCertificateTrustCallback((ctx) =>
@@ -86,4 +90,146 @@ private static IResourceBuilder WithNodeDefaults(this IResource
 
                 return Task.CompletedTask;
             });
+
+    /// 
+    /// Adds a Vite app to the distributed application builder.
+    /// 
+    /// The  to add the resource to.
+    /// The name of the Vite app.
+    /// The working directory of the Vite app.
+    /// When true use HTTPS for the endpoints, otherwise use HTTP.
+    /// A reference to the .
+    /// 
+    /// 
+    /// The following example creates a Vite app using npm as the package manager.
+    /// 
+    /// var builder = DistributedApplication.CreateBuilder(args);
+    ///
+    /// builder.AddViteApp("frontend", "./frontend")
+    ///        .WithNpmPackageManager();
+    ///
+    /// builder.Build().Run();
+    /// 
+    /// 
+    /// 
+    public static IResourceBuilder AddViteApp(this IDistributedApplicationBuilder builder, [ResourceName] string name, string workingDirectory, bool useHttps = false)
+    {
+        ArgumentNullException.ThrowIfNull(builder);
+        ArgumentException.ThrowIfNullOrEmpty(name);
+        ArgumentException.ThrowIfNullOrEmpty(workingDirectory);
+
+        workingDirectory = PathNormalizer.NormalizePathForCurrentPlatform(Path.Combine(builder.AppHostDirectory, workingDirectory));
+        var resource = new ViteAppResource(name, "node", workingDirectory);
+
+        var resourceBuilder = builder.AddResource(resource)
+            .WithNodeDefaults()
+            .WithIconName("CodeJsRectangle")
+            .WithArgs(c =>
+            {
+                if (resource.TryGetLastAnnotation(out var packageManagerAnnotation))
+                {
+                    foreach (var arg in packageManagerAnnotation.RunCommandLineArgs)
+                    {
+                        c.Args.Add(arg);
+                    }
+                }
+                c.Args.Add("dev");
+
+                if (packageManagerAnnotation?.CommandSeparator is string separator)
+                {
+                    c.Args.Add(separator);
+                }
+
+                var targetEndpoint = resource.GetEndpoint("https");
+                if (!targetEndpoint.Exists)
+                {
+                    targetEndpoint = resource.GetEndpoint("http");
+                }
+
+                c.Args.Add("--port");
+                c.Args.Add(targetEndpoint.Property(EndpointProperty.TargetPort));
+            });
+
+        _ = useHttps
+            ? resourceBuilder.WithHttpsEndpoint(env: "PORT")
+            : resourceBuilder.WithHttpEndpoint(env: "PORT");
+
+        return resourceBuilder
+            .AddNpmPackageManagerAnnotation(useCI: false)
+            .PublishAsDockerFile(c =>
+            {
+                // Only generate a Dockerfile if one doesn't already exist in the app directory
+                if (File.Exists(Path.Combine(resource.WorkingDirectory, "Dockerfile")))
+                {
+                    return;
+                }
+
+                c.WithDockerfileBuilder(resource.WorkingDirectory, dockerfileContext =>
+                {
+                    if (c.Resource.TryGetLastAnnotation(out var packageManagerAnnotation)
+                        && packageManagerAnnotation.BuildCommandLineArgs is { Length: > 0 })
+                    {
+                        var dockerBuilder = dockerfileContext.Builder
+                            .From("node:22-slim")
+                            .WorkDir("/app")
+                            .Copy(".", ".");
+
+                        if (packageManagerAnnotation.InstallCommandLineArgs is { Length: > 0 })
+                        {
+                            dockerBuilder
+                                .Run($"{resourceBuilder.Resource.Command} {string.Join(' ', packageManagerAnnotation.InstallCommandLineArgs)}");
+                        }
+                        dockerBuilder
+                                .Run($"{resourceBuilder.Resource.Command} {string.Join(' ', packageManagerAnnotation.BuildCommandLineArgs)}");
+                    }
+                });
+            });
+    }
+
+    /// 
+    /// Ensures the Node.js packages are installed before the application starts using npm as the package manager.
+    /// 
+    /// The NodeAppResource.
+    /// When true, use npm ci, otherwise use npm install when installing packages.
+    /// Configure the npm installer resource.
+    /// A reference to the .
+    public static IResourceBuilder WithNpmPackageManager(this IResourceBuilder resource, bool useCI = false, Action>? configureInstaller = null) where TResource : NodeAppResource
+    {
+        AddNpmPackageManagerAnnotation(resource, useCI);
+
+        // Only install packages during development, not in publish mode
+        if (!resource.ApplicationBuilder.ExecutionContext.IsPublishMode)
+        {
+            var installerName = $"{resource.Resource.Name}-npm-install";
+            var installer = new NodeInstallerResource(installerName, resource.Resource.WorkingDirectory);
+
+            var installerBuilder = resource.ApplicationBuilder.AddResource(installer)
+                .WithCommand("npm")
+                .WithArgs([useCI ? "ci" : "install"])
+                .WithParentRelationship(resource.Resource)
+                .ExcludeFromManifest();
+
+            // Make the parent resource wait for the installer to complete
+            resource.WaitForCompletion(installerBuilder);
+
+            configureInstaller?.Invoke(installerBuilder);
+
+            resource.WithAnnotation(new JavaScriptPackageInstallerAnnotation(installer));
+        }
+
+        return resource;
+    }
+
+    private static IResourceBuilder AddNpmPackageManagerAnnotation(this IResourceBuilder resource, bool useCI) where TResource : NodeAppResource
+    {
+        resource.WithCommand("npm");
+        resource.WithAnnotation(new JavaScriptPackageManagerAnnotation("npm")
+        {
+            InstallCommandLineArgs = [useCI ? "ci" : "install"],
+            RunCommandLineArgs = ["run"],
+            BuildCommandLineArgs = ["run", "build"]
+        });
+
+        return resource;
+    }
 }
diff --git a/src/Aspire.Hosting.NodeJs/NodeInstallerResource.cs b/src/Aspire.Hosting.NodeJs/NodeInstallerResource.cs
new file mode 100644
index 00000000000..c55bb5a6432
--- /dev/null
+++ b/src/Aspire.Hosting.NodeJs/NodeInstallerResource.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+
+namespace Aspire.Hosting.NodeJs;
+
+/// 
+/// A resource that represents a package installer for a node app.
+/// 
+/// The name of the resource.
+/// The working directory to use for the command.
+public class NodeInstallerResource(string name, string workingDirectory)
+    : ExecutableResource(name, "node", workingDirectory);
diff --git a/src/Aspire.Hosting.NodeJs/ViteAppResource.cs b/src/Aspire.Hosting.NodeJs/ViteAppResource.cs
new file mode 100644
index 00000000000..6ca2b1690dc
--- /dev/null
+++ b/src/Aspire.Hosting.NodeJs/ViteAppResource.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.Hosting.NodeJs;
+
+/// 
+/// Represents a Vite application resource that can be managed and executed within a Node.js environment.
+/// 
+/// The unique name used to identify the Vite application resource.
+/// The command to execute the Vite application, such as the script or entry point.
+/// The working directory from which the Vite application command is executed.
+public class ViteAppResource(string name, string command, string workingDirectory)
+    : NodeAppResource(name, command, workingDirectory);
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/13.0/apphost.cs b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/13.0/apphost.cs
index 3d92e979c79..4824a97aa32 100644
--- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/13.0/apphost.cs
+++ b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/13.0/apphost.cs
@@ -4,7 +4,6 @@
 #if UseRedisCache
 #:package Aspire.Hosting.Redis@!!REPLACE_WITH_LATEST_VERSION!!
 #endif
-#:package CommunityToolkit.Aspire.Hosting.NodeJS.Extensions@9.8.0
 
 #pragma warning disable ASPIREHOSTINGPYTHON001
 
@@ -29,7 +28,7 @@
     });
 
 builder.AddViteApp("frontend", "./frontend")
-    .WithNpmPackageInstallation()
+    .WithNpmPackageManager()
     .WithReference(apiService)
     .WaitFor(apiService);
 
diff --git a/tests/Aspire.Hosting.NodeJs.Tests/AddViteAppTests.cs b/tests/Aspire.Hosting.NodeJs.Tests/AddViteAppTests.cs
new file mode 100644
index 00000000000..155f723b657
--- /dev/null
+++ b/tests/Aspire.Hosting.NodeJs.Tests/AddViteAppTests.cs
@@ -0,0 +1,55 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.Utils;
+
+namespace Aspire.Hosting.NodeJs.Tests;
+
+public class AddViteAppTests
+{
+    [Fact]
+    public async Task VerifyDefaultDockerfile()
+    {
+        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish).WithResourceCleanUp(true);
+
+        var workingDirectory = AppContext.BaseDirectory;
+        var nodeApp = builder.AddViteApp("vite", "vite")
+            .WithNpmPackageManager();
+
+        var manifest = await ManifestUtils.GetManifest(nodeApp.Resource);
+
+        var expectedManifest = $$"""
+            {
+              "type": "container.v1",
+              "build": {
+                "context": "../../../../../tests/Aspire.Hosting.Tests/vite",
+                "dockerfile": "vite.Dockerfile"
+              },
+              "env": {
+                "NODE_ENV": "production",
+                "PORT": "{vite.bindings.http.targetPort}"
+              },
+              "bindings": {
+                "http": {
+                  "scheme": "http",
+                  "protocol": "tcp",
+                  "transport": "http",
+                  "targetPort": 8000
+                }
+              }
+            }
+            """;
+        Assert.Equal(expectedManifest, manifest.ToString());
+
+        var dockerfileContents = File.ReadAllText("vite.Dockerfile");
+        var expectedDockerfile = $$"""
+            FROM node:22-slim
+            WORKDIR /app
+            COPY . .
+            RUN npm install
+            RUN npm run build
+
+            """.Replace("\r\n", "\n");
+        Assert.Equal(expectedDockerfile, dockerfileContents);
+    }
+}
diff --git a/tests/Aspire.Hosting.NodeJs.Tests/IntegrationTests.cs b/tests/Aspire.Hosting.NodeJs.Tests/IntegrationTests.cs
new file mode 100644
index 00000000000..4beae9b1200
--- /dev/null
+++ b/tests/Aspire.Hosting.NodeJs.Tests/IntegrationTests.cs
@@ -0,0 +1,81 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Aspire.Hosting.NodeJs.Tests;
+
+/// 
+/// Integration test that demonstrates the new resource-based package installer architecture.
+/// This shows how installer resources appear as separate resources in the application model.
+/// 
+public class IntegrationTests
+{
+    [Fact]
+    public void ResourceBasedPackageInstallersAppearInApplicationModel()
+    {
+        var builder = DistributedApplication.CreateBuilder();
+
+        // Add a vite app with the npm package manager
+        builder.AddViteApp("vite-app", "./frontend")
+            .WithNpmPackageManager(useCI: true);
+
+        using var app = builder.Build();
+        var appModel = app.Services.GetRequiredService();
+
+        // Verify all Node.js app resources are present
+        var nodeResources = appModel.Resources.OfType().ToList();
+        Assert.Single(nodeResources);
+
+        // Verify all installer resources are present as separate resources
+        var npmInstallers = appModel.Resources.OfType().ToList();
+
+        Assert.Single(npmInstallers);
+
+        // Verify installer resources have expected names (would appear on dashboard)
+        Assert.Equal("vite-app-npm-install", npmInstallers[0].Name);
+
+        // Verify parent-child relationships
+        foreach (var installer in npmInstallers.Cast())
+        {
+            Assert.True(installer.TryGetAnnotationsOfType(out var relationships));
+            Assert.Single(relationships);
+            Assert.Equal("Parent", relationships.First().Type);
+        }
+
+        // Verify all Node.js apps wait for their installers
+        foreach (var nodeApp in nodeResources)
+        {
+            Assert.True(nodeApp.TryGetAnnotationsOfType(out var waitAnnotations));
+            Assert.Single(waitAnnotations);
+
+            var waitedResource = waitAnnotations.First().Resource;
+            Assert.True(waitedResource is NodeInstallerResource);
+        }
+    }
+
+    [Fact]
+    public void InstallerResourcesHaveCorrectExecutableConfiguration()
+    {
+        var builder = DistributedApplication.CreateBuilder();
+
+        builder.AddNpmApp("test-app", "./test")
+            .WithNpmPackageManager(useCI: true);
+
+        using var app = builder.Build();
+        var appModel = app.Services.GetRequiredService();
+
+        var installer = Assert.Single(appModel.Resources.OfType());
+
+        // Verify it's configured as an ExecutableResource
+        Assert.IsAssignableFrom(installer);
+
+        // Verify working directory matches parent
+        var parentApp = Assert.Single(appModel.Resources.OfType());
+        Assert.Equal(parentApp.WorkingDirectory, installer.WorkingDirectory);
+
+        // Verify command arguments are configured
+        Assert.True(installer.TryGetAnnotationsOfType(out var argsAnnotations));
+    }
+}
diff --git a/tests/Aspire.Hosting.NodeJs.Tests/PackageInstallationTests.cs b/tests/Aspire.Hosting.NodeJs.Tests/PackageInstallationTests.cs
new file mode 100644
index 00000000000..5bf8abd794d
--- /dev/null
+++ b/tests/Aspire.Hosting.NodeJs.Tests/PackageInstallationTests.cs
@@ -0,0 +1,121 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Aspire.Hosting.NodeJs.Tests;
+
+public class PackageInstallationTests
+{
+    /// 
+    /// This test validates that the WithNpmPackageManager method creates
+    /// installer resources with proper arguments and relationships.
+    /// 
+    [Fact]
+    public async Task WithNpmPackageManager_CanBeConfiguredWithInstallAndCIOptions()
+    {
+        var builder = DistributedApplication.CreateBuilder();
+
+        var nodeApp = builder.AddNpmApp("test-app", "./test-app");
+        var nodeApp2 = builder.AddNpmApp("test-app-ci", "./test-app-ci");
+
+        // Test that both configurations can be set up without errors
+        nodeApp.WithNpmPackageManager(useCI: false); // Uses npm install
+        nodeApp2.WithNpmPackageManager(useCI: true);  // Uses npm ci
+
+        using var app = builder.Build();
+
+        var appModel = app.Services.GetRequiredService();
+        var nodeResources = appModel.Resources.OfType().ToList();
+        var installerResources = appModel.Resources.OfType().ToList();
+
+        Assert.Equal(2, nodeResources.Count);
+        Assert.Equal(2, installerResources.Count);
+        Assert.All(nodeResources, resource => Assert.Equal("npm", resource.Command));
+
+        // Verify install vs ci commands
+        var installResource = installerResources.Single(r => r.Name == "test-app-npm-install");
+        var ciResource = installerResources.Single(r => r.Name == "test-app-ci-npm-install");
+
+        Assert.Equal("npm", installResource.Command);
+        var args = await installResource.GetArgumentValuesAsync();
+        Assert.Single(args);
+        Assert.Equal("install", args[0]);
+
+        Assert.Equal("npm", ciResource.Command);
+        args = await ciResource.GetArgumentValuesAsync();
+        Assert.Single(args);
+        Assert.Equal("ci", args[0]);
+    }
+
+    [Fact]
+    public void WithNpmPackageManager_ExcludedFromPublishMode()
+    {
+        var builder = DistributedApplication.CreateBuilder(["Publishing:Publisher=manifest", "Publishing:OutputPath=./publish"]);
+
+        var nodeApp = builder.AddNpmApp("test-app", "./test-app");
+        nodeApp.WithNpmPackageManager(useCI: false);
+
+        using var app = builder.Build();
+
+        var appModel = app.Services.GetRequiredService();
+
+        // Verify the NodeApp resource exists
+        var nodeResource = Assert.Single(appModel.Resources.OfType());
+        Assert.Equal("npm", nodeResource.Command);
+
+        // Verify NO installer resource was created in publish mode
+        var installerResources = appModel.Resources.OfType().ToList();
+        Assert.Empty(installerResources);
+
+        // Verify no wait annotations were added
+        Assert.False(nodeResource.TryGetAnnotationsOfType(out _));
+    }
+
+    [Fact]
+    public async Task WithNpmPackageManager_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.WithNpmPackageManager(useCI: false, configureInstaller: installerBuilder =>
+        {
+            installerBuilder.WithArgs("--legacy-peer-deps");
+        });
+        nodeAppWithArgs.WithNpmPackageManager(useCI: true, configureInstaller: installerBuilder =>
+        {
+            installerBuilder.WithArgs("--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)
+        );
+    }
+}
diff --git a/tests/Aspire.Hosting.NodeJs.Tests/ResourceCreationTests.cs b/tests/Aspire.Hosting.NodeJs.Tests/ResourceCreationTests.cs
new file mode 100644
index 00000000000..2d0da25f65d
--- /dev/null
+++ b/tests/Aspire.Hosting.NodeJs.Tests/ResourceCreationTests.cs
@@ -0,0 +1,214 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Aspire.Hosting.NodeJs.Tests;
+
+public class ResourceCreationTests
+{
+    [Fact]
+    public void DefaultViteAppUsesNpm()
+    {
+        var builder = DistributedApplication.CreateBuilder();
+
+        builder.AddViteApp("vite", "vite");
+
+        using var app = builder.Build();
+
+        var appModel = app.Services.GetRequiredService();
+
+        var resource = appModel.Resources.OfType().SingleOrDefault();
+
+        Assert.NotNull(resource);
+
+        Assert.Equal("npm", resource.Command);
+    }
+
+    [Fact]
+    public void ViteAppUsesSpecifiedWorkingDirectory()
+    {
+        var builder = DistributedApplication.CreateBuilder();
+
+        builder.AddViteApp("vite", "test");
+
+        using var app = builder.Build();
+
+        var appModel = app.Services.GetRequiredService();
+
+        var resource = appModel.Resources.OfType().SingleOrDefault();
+
+        Assert.NotNull(resource);
+
+        Assert.Equal(Path.Combine(builder.AppHostDirectory, "test"), resource.WorkingDirectory);
+    }
+
+    [Fact]
+    public void ViteAppHasExposedHttpEndpoints()
+    {
+        var builder = DistributedApplication.CreateBuilder();
+
+        builder.AddViteApp("vite", "vite");
+
+        using var app = builder.Build();
+
+        var appModel = app.Services.GetRequiredService();
+
+        var resource = appModel.Resources.OfType().SingleOrDefault();
+
+        Assert.NotNull(resource);
+
+        Assert.True(resource.TryGetAnnotationsOfType(out var endpoints));
+
+        Assert.Contains(endpoints, e => e.UriScheme == "http");
+    }
+
+    [Fact]
+    public void ViteAppHasExposedHttpsEndpoints()
+    {
+        var builder = DistributedApplication.CreateBuilder();
+
+        builder.AddViteApp("vite", "vite", useHttps: true);
+
+        using var app = builder.Build();
+
+        var appModel = app.Services.GetRequiredService();
+
+        var resource = appModel.Resources.OfType().SingleOrDefault();
+
+        Assert.NotNull(resource);
+
+        Assert.True(resource.TryGetAnnotationsOfType(out var endpoints));
+
+        Assert.Contains(endpoints, e => e.UriScheme == "https");
+    }
+
+    [Fact]
+    public void ViteAppDoesNotExposeExternalHttpEndpointsByDefault()
+    {
+        var builder = DistributedApplication.CreateBuilder();
+
+        builder.AddViteApp("vite", "vite");
+
+        using var app = builder.Build();
+
+        var appModel = app.Services.GetRequiredService();
+
+        var resource = appModel.Resources.OfType().SingleOrDefault();
+
+        Assert.NotNull(resource);
+
+        Assert.True(resource.TryGetAnnotationsOfType(out var endpoints));
+
+        Assert.DoesNotContain(endpoints, e => e.IsExternal);
+    }
+
+    [Fact]
+    public async Task WithNpmPackageManagerDefaultsToInstallCommand()
+    {
+        var builder = DistributedApplication.CreateBuilder();
+
+        var nodeApp = builder.AddNpmApp("test-app", "./test-app");
+
+        // Add package installation with default settings (should use npm install, not ci)
+        nodeApp.WithNpmPackageManager(useCI: false);
+
+        using var app = builder.Build();
+
+        var appModel = app.Services.GetRequiredService();
+
+        // Verify the NodeApp resource exists
+        var nodeResource = Assert.Single(appModel.Resources.OfType());
+        Assert.Equal("npm", nodeResource.Command);
+
+        // Verify the installer resource was created
+        var installerResource = Assert.Single(appModel.Resources.OfType());
+        Assert.Equal("test-app-npm-install", installerResource.Name);
+        Assert.Equal("npm", installerResource.Command);
+        var args = await installerResource.GetArgumentValuesAsync();
+        Assert.Single(args);
+        Assert.Equal("install", args[0]);
+
+        // Verify the parent-child relationship
+        Assert.True(installerResource.TryGetAnnotationsOfType(out var relationships));
+        var relationship = Assert.Single(relationships);
+        Assert.Same(nodeResource, relationship.Resource);
+        Assert.Equal("Parent", relationship.Type);
+
+        // Verify the wait annotation on the parent
+        Assert.True(nodeResource.TryGetAnnotationsOfType(out var waitAnnotations));
+        var waitAnnotation = Assert.Single(waitAnnotations);
+        Assert.Same(installerResource, waitAnnotation.Resource);
+    }
+
+    [Fact]
+    public async Task WithNpmPackageManagerCanUseCICommand()
+    {
+        var builder = DistributedApplication.CreateBuilder();
+
+        var nodeApp = builder.AddNpmApp("test-app", "./test-app");
+
+        // Add package installation with CI enabled
+        nodeApp.WithNpmPackageManager(useCI: true);
+
+        using var app = builder.Build();
+
+        var appModel = app.Services.GetRequiredService();
+
+        // Verify the NodeApp resource exists
+        var nodeResource = Assert.Single(appModel.Resources.OfType());
+        Assert.Equal("npm", nodeResource.Command);
+
+        // Verify the installer resource was created with CI enabled
+        var installerResource = Assert.Single(appModel.Resources.OfType());
+        Assert.Equal("test-app-npm-install", installerResource.Name);
+        Assert.Equal("npm", installerResource.Command);
+        var args = await installerResource.GetArgumentValuesAsync();
+        Assert.Single(args);
+        Assert.Equal("ci", args[0]);
+
+        // Verify the parent-child relationship
+        Assert.True(installerResource.TryGetAnnotationsOfType(out var relationships));
+        var relationship = Assert.Single(relationships);
+        Assert.Same(nodeResource, relationship.Resource);
+        Assert.Equal("Parent", relationship.Type);
+
+        // Verify the wait annotation on the parent
+        Assert.True(nodeResource.TryGetAnnotationsOfType(out var waitAnnotations));
+        var waitAnnotation = Assert.Single(waitAnnotations);
+        Assert.Same(installerResource, waitAnnotation.Resource);
+    }
+
+    [Fact]
+    public void ViteAppConfiguresPortFromEnvironment()
+    {
+        var builder = DistributedApplication.CreateBuilder();
+
+        builder.AddViteApp("vite", "vite");
+
+        using var app = builder.Build();
+
+        var appModel = app.Services.GetRequiredService();
+
+        var resource = Assert.Single(appModel.Resources.OfType());
+
+        // Verify that command line arguments callback is configured
+        Assert.True(resource.TryGetAnnotationsOfType(out var argsCallbackAnnotations));
+        List