Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 62 additions & 1 deletion src/Aspire.Cli/Projects/TypeScriptAppHostToolchainResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ internal static class TypeScriptAppHostToolchainResolver
private const string BunLockFileName = "bun.lock";
private const string BunBinaryLockFileName = "bun.lockb";
private const string YarnLockFileName = "yarn.lock";
private const string YarnClassicLockFileVersionLine = "# yarn lockfile v1";
private const string YarnConfigFileName = ".yarnrc.yml";
private const string PackageLockFileName = "package-lock.json";
private const string PnpmLockFileName = "pnpm-lock.yaml";
Expand Down Expand Up @@ -70,8 +71,14 @@ internal static TypeScriptAppHostToolchainResolution ResolveWithReason(Directory
return CreateLockFileResolution(TypeScriptAppHostToolchain.Pnpm, PnpmLockFileName, candidateDirectory);
}

if (File.Exists(Path.Combine(candidateDirectory.FullName, YarnLockFileName)))
var yarnLockFilePath = Path.Combine(candidateDirectory.FullName, YarnLockFileName);
if (File.Exists(yarnLockFilePath))
{
if (IsYarnClassicLockFile(yarnLockFilePath))
{
throw CreateYarnClassicNotSupportedException($"the Yarn lockfile at {yarnLockFilePath}");
}

return CreateLockFileResolution(TypeScriptAppHostToolchain.Yarn, YarnLockFileName, candidateDirectory);
}

Expand Down Expand Up @@ -265,6 +272,11 @@ private static bool TryGetToolchainFromPackageJson(DirectoryInfo appHostDirector
var packageManagerName = packageManager.Split('@', 2)[0];
if (TryParseToolchain(packageManagerName, out toolchain))
{
if (toolchain == TypeScriptAppHostToolchain.Yarn && IsYarnClassicPackageManager(packageManager))
{
throw CreateYarnClassicNotSupportedException($"'{packageManager}' in {packageJsonPath}");
Comment thread
eerhardt marked this conversation as resolved.
}

reason = $"packageManager '{packageManager}' found in {packageJsonPath}";
return true;
}
Expand Down Expand Up @@ -294,6 +306,55 @@ private static bool TryParseToolchain(string packageManagerName, out TypeScriptA
return result.HasValue;
}

private static bool IsYarnClassicPackageManager(string packageManager)
{
const string yarnPackageManagerPrefix = "yarn@";

if (!packageManager.StartsWith(yarnPackageManagerPrefix, StringComparison.OrdinalIgnoreCase))
{
return false;
}

var version = packageManager[yarnPackageManagerPrefix.Length..];
return version.Length > 0 &&
version[0] == '1' &&
(version.Length == 1 || !char.IsAsciiDigit(version[1]));
}

private static InvalidOperationException CreateYarnClassicNotSupportedException(string upgradeTarget)
{
return new InvalidOperationException(
$"Yarn Classic is not supported for TypeScript AppHosts. Upgrade {upgradeTarget} to Yarn 4 or later, or use npm, pnpm, or Bun.");
}

private static bool IsYarnClassicLockFile(string yarnLockFilePath)
{
try
{
var linesRead = 0;
foreach (var line in File.ReadLines(yarnLockFilePath))
{
if (line.Trim().Equals(YarnClassicLockFileVersionLine, StringComparison.OrdinalIgnoreCase))
{
return true;
}

linesRead++;
if (linesRead >= 5)
{
return false;
}
}
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException
or SecurityException or NotSupportedException)
{
return false;
}

return false;
}

private static IEnumerable<DirectoryInfo> EnumerateCandidateDirectories(DirectoryInfo appHostDirectory)
{
yield return appHostDirectory;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ internal static string GetPackageManager(string toolchain) =>
NormalizeToolchain(toolchain) switch
{
"bun" => "bun@1.2.0",
"yarn" => "yarn@1.22.22",
"yarn" => "yarn@4.14.1",
"pnpm" => "pnpm@10.0.0",
"npm" => "npm@10.0.0",
_ => throw new ArgumentOutOfRangeException(nameof(toolchain), toolchain, "Unsupported TypeScript AppHost toolchain.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,58 @@ public void Resolve_WhenPnpmLockExists_ReturnsPnpm()
Assert.Equal(TypeScriptAppHostToolchain.Pnpm, toolchain);
}

[Fact]
public void Resolve_WhenPackageManagerIsYarnClassic_Throws()
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
var packageJsonPath = Path.Combine(workspace.WorkspaceRoot.FullName, "package.json");
File.WriteAllText(packageJsonPath, "{ \"packageManager\": \"yarn@1.22.22\" }");

var exception = Assert.Throws<InvalidOperationException>(() => TypeScriptAppHostToolchainResolver.Resolve(workspace.WorkspaceRoot, logger: null));

Assert.Equal($"Yarn Classic is not supported for TypeScript AppHosts. Upgrade 'yarn@1.22.22' in {packageJsonPath} to Yarn 4 or later, or use npm, pnpm, or Bun.", exception.Message);
}

[Fact]
public void Resolve_WhenPackageManagerIsModernYarn_ReturnsYarn()
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
File.WriteAllText(Path.Combine(workspace.WorkspaceRoot.FullName, "package.json"), "{ \"packageManager\": \"yarn@4.14.1\" }");

var toolchain = TypeScriptAppHostToolchainResolver.Resolve(workspace.WorkspaceRoot, logger: null);

Assert.Equal(TypeScriptAppHostToolchain.Yarn, toolchain);
}

[Fact]
public void Resolve_WhenYarnLockIsClassic_Throws()
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
File.WriteAllText(Path.Combine(workspace.WorkspaceRoot.FullName, "package.json"), "{ \"name\": \"apphost\" }");
var yarnLockPath = Path.Combine(workspace.WorkspaceRoot.FullName, "yarn.lock");
File.WriteAllText(yarnLockPath, "# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n# yarn lockfile v1\n");

var exception = Assert.Throws<InvalidOperationException>(() => TypeScriptAppHostToolchainResolver.Resolve(workspace.WorkspaceRoot, logger: null));

Assert.Equal($"Yarn Classic is not supported for TypeScript AppHosts. Upgrade the Yarn lockfile at {yarnLockPath} to Yarn 4 or later, or use npm, pnpm, or Bun.", exception.Message);
}

[Fact]
public void Resolve_WhenParentYarnLockIsClassic_Throws()
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
var appHostDirectory = workspace.WorkspaceRoot.CreateSubdirectory("apps").CreateSubdirectory("apphost");
var parentDirectory = appHostDirectory.Parent!;
File.WriteAllText(Path.Combine(appHostDirectory.FullName, "package.json"), "{ \"name\": \"apphost\" }");
File.WriteAllText(Path.Combine(parentDirectory.FullName, "package.json"), "{ \"name\": \"workspace\" }");
var yarnLockPath = Path.Combine(parentDirectory.FullName, "yarn.lock");
File.WriteAllText(yarnLockPath, "# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n# yarn lockfile v1\n");

var exception = Assert.Throws<InvalidOperationException>(() => TypeScriptAppHostToolchainResolver.Resolve(appHostDirectory, logger: null));

Assert.Equal($"Yarn Classic is not supported for TypeScript AppHosts. Upgrade the Yarn lockfile at {yarnLockPath} to Yarn 4 or later, or use npm, pnpm, or Bun.", exception.Message);
}

[Fact]
public void Resolve_WhenPackageLockExists_ReturnsNpm()
{
Expand Down
4 changes: 3 additions & 1 deletion tests/Shared/Docker/Dockerfile.e2e
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ RUN apt-get update -qq && \
# Install Node.js plus supported TypeScript AppHost toolchains.
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
apt-get install -y --no-install-recommends nodejs && \
npm install -g yarn@1.22.22 pnpm@10 && \
corepack enable yarn && \
corepack prepare yarn@4.14.1 --activate && \
npm install -g pnpm@10 && \
curl -fsSL https://bun.sh/install | bash && \
ln -s /root/.bun/bin/bun /usr/local/bin/bun && \
ln -s /root/.bun/bin/bunx /usr/local/bin/bunx && \
Expand Down
4 changes: 3 additions & 1 deletion tests/Shared/Docker/Dockerfile.e2e-polyglot-base
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
# Install Node.js plus supported TypeScript AppHost toolchains.
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
apt-get install -y --no-install-recommends nodejs && \
npm install -g yarn@1.22.22 pnpm@10 && \
corepack enable yarn && \
corepack prepare yarn@4.14.1 --activate && \
npm install -g pnpm@10 && \
curl -fsSL https://bun.sh/install | bash && \
ln -s /root/.bun/bin/bun /usr/local/bin/bun && \
ln -s /root/.bun/bin/bunx /usr/local/bin/bunx && \
Expand Down
Loading