diff --git a/src/Aspire.Hosting.NodeJs/NodeExtensions.cs b/src/Aspire.Hosting.NodeJs/NodeExtensions.cs index e4d5f9b9731..4a5921681f5 100644 --- a/src/Aspire.Hosting.NodeJs/NodeExtensions.cs +++ b/src/Aspire.Hosting.NodeJs/NodeExtensions.cs @@ -475,11 +475,29 @@ public static IResourceBuilder WithYarn(this IResourceBuil return resource; } - private static string[] GetDefaultYarnInstallArgs(IResourceBuilder resource) => - resource.ApplicationBuilder.ExecutionContext.IsPublishMode && - File.Exists(Path.Combine(resource.Resource.WorkingDirectory, "yarn.lock")) - ? ["--immutable"] - : []; + private static string[] GetDefaultYarnInstallArgs(IResourceBuilder resource) + { + var workingDirectory = resource.Resource.WorkingDirectory; + if (!resource.ApplicationBuilder.ExecutionContext.IsPublishMode || + !File.Exists(Path.Combine(workingDirectory, "yarn.lock"))) + { + // Not publish mode or no yarn.lock, use default install args + return []; + } + + var yarnRcYml = Path.Combine(workingDirectory, ".yarnrc.yml"); + var yarnBerryReleaseDir = Path.Combine(workingDirectory, ".yarn", "releases"); + var hasYarnBerry = File.Exists(yarnRcYml) || Directory.Exists(yarnBerryReleaseDir); + + if (hasYarnBerry) + { + // Yarn 2+ detected, --frozen-lockfile is deprecated in v2+, use --immutable instead + return ["--immutable"]; + } + + // Fallback: default to Yarn v1.x behavior + return ["--frozen-lockfile"]; + } /// /// Configures the Node.js resource to use pnmp as the package manager and optionally installs packages before the application starts. diff --git a/tests/Aspire.Hosting.NodeJs.Tests/PackageInstallationTests.cs b/tests/Aspire.Hosting.NodeJs.Tests/PackageInstallationTests.cs index 59a0af6ac4a..cffba9853db 100644 --- a/tests/Aspire.Hosting.NodeJs.Tests/PackageInstallationTests.cs +++ b/tests/Aspire.Hosting.NodeJs.Tests/PackageInstallationTests.cs @@ -476,7 +476,7 @@ public void WithYarn_DefaultsArgsInPublishMode() .WithYarn(); Assert.True(app.Resource.TryGetLastAnnotation(out var installCommand)); - Assert.Equal(["install", "--immutable"], installCommand.Args); + Assert.Equal(["install", "--frozen-lockfile"], installCommand.Args); var app2 = builder.AddViteApp("test-app2", tempDir.Path) .WithYarn(installArgs: ["--immutable-cache"]); @@ -485,6 +485,38 @@ public void WithYarn_DefaultsArgsInPublishMode() Assert.Equal(["install", "--immutable-cache"], installCommand.Args); } + [Fact] + public void WithYarn_ReturnsImmutable_WhenYarnRcYmlExists() + { + using var tempDir = new TempDirectory(); + File.WriteAllText(Path.Combine(tempDir.Path, "yarn.lock"), "empty"); + File.WriteAllText(Path.Combine(tempDir.Path, ".yarnrc.yml"), "empty"); + + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var app = builder.AddViteApp("test-app", tempDir.Path) + .WithYarn(); + + Assert.True(app.Resource.TryGetLastAnnotation(out var installCommand)); + Assert.Equal(["install", "--immutable"], installCommand.Args); + } + + [Fact] + public void WithYarn_ReturnsImmutable_WhenYarnReleasesDirExists() + { + using var tempDir = new TempDirectory(); + File.WriteAllText(Path.Combine(tempDir.Path, "yarn.lock"), "empty"); + Directory.CreateDirectory(Path.Combine(tempDir.Path, ".yarn", "releases")); + + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var app = builder.AddViteApp("test-app", tempDir.Path) + .WithYarn(); + + Assert.True(app.Resource.TryGetLastAnnotation(out var installCommand)); + Assert.Equal(["install", "--immutable"], installCommand.Args); + } + [Fact] public void WithPnpm_DefaultsArgsInPublishMode() {