Skip to content

Commit b9b3e65

Browse files
committed
add assets to csproj
1 parent d54b4ff commit b9b3e65

File tree

5 files changed

+141
-0
lines changed

5 files changed

+141
-0
lines changed

src/winapp-CLI/WinApp.Cli.Tests/DotNetServiceTests.cs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,4 +1016,88 @@ [new DotNetPackage("Microsoft.WindowsAppSDK", "1.6.0", "1.6.0")])
10161016
}
10171017

10181018
#endregion
1019+
1020+
#region EnsureAssetContentItemsAsync
1021+
1022+
[TestMethod]
1023+
public async Task EnsureAssetContentItems_AddsMissingAssetsGlob()
1024+
{
1025+
// Arrange
1026+
var csprojPath = Path.Combine(_testTempDirectory, "Test.csproj");
1027+
File.WriteAllText(csprojPath, @"<Project Sdk=""Microsoft.NET.Sdk"">
1028+
<PropertyGroup>
1029+
<TargetFramework>net10.0</TargetFramework>
1030+
</PropertyGroup>
1031+
</Project>
1032+
");
1033+
1034+
// Act
1035+
var result = await _dotNetService.EnsureAssetContentItemsAsync(
1036+
new FileInfo(csprojPath), TestContext.CancellationToken);
1037+
1038+
// Assert
1039+
Assert.IsTrue(result, "Should modify csproj when no asset Content items exist");
1040+
var content = File.ReadAllText(csprojPath);
1041+
Assert.IsTrue(content.Contains(@"<Content Include=""Assets\**"" />"),
1042+
"Should add Assets glob Content item");
1043+
Assert.IsTrue(content.Contains("</Project>"),
1044+
"Should preserve </Project> closing tag");
1045+
}
1046+
1047+
[TestMethod]
1048+
public async Task EnsureAssetContentItems_SkipsWhenAlreadyPresent()
1049+
{
1050+
// Arrange
1051+
var csprojPath = Path.Combine(_testTempDirectory, "Test.csproj");
1052+
File.WriteAllText(csprojPath, @"<Project Sdk=""Microsoft.NET.Sdk"">
1053+
<PropertyGroup>
1054+
<TargetFramework>net10.0</TargetFramework>
1055+
</PropertyGroup>
1056+
<ItemGroup>
1057+
<Content Include=""Assets\**"" />
1058+
</ItemGroup>
1059+
</Project>
1060+
");
1061+
1062+
// Act
1063+
var result = await _dotNetService.EnsureAssetContentItemsAsync(
1064+
new FileInfo(csprojPath), TestContext.CancellationToken);
1065+
1066+
// Assert
1067+
Assert.IsFalse(result, "Should not modify csproj when asset Content items already exist");
1068+
}
1069+
1070+
[TestMethod]
1071+
public async Task EnsureAssetContentItems_SkipsWhenIndividualAssetPresent()
1072+
{
1073+
// Arrange
1074+
var csprojPath = Path.Combine(_testTempDirectory, "Test.csproj");
1075+
File.WriteAllText(csprojPath, @"<Project Sdk=""Microsoft.NET.Sdk"">
1076+
<ItemGroup>
1077+
<Content Include=""Assets\StoreLogo.png"" />
1078+
</ItemGroup>
1079+
</Project>
1080+
");
1081+
1082+
// Act
1083+
var result = await _dotNetService.EnsureAssetContentItemsAsync(
1084+
new FileInfo(csprojPath), TestContext.CancellationToken);
1085+
1086+
// Assert
1087+
Assert.IsFalse(result, "Should not modify csproj when individual asset Content items exist");
1088+
}
1089+
1090+
[TestMethod]
1091+
public async Task EnsureAssetContentItems_ReturnsFalseForMissingFile()
1092+
{
1093+
// Act
1094+
var result = await _dotNetService.EnsureAssetContentItemsAsync(
1095+
new FileInfo(Path.Combine(_testTempDirectory, "NonExistent.csproj")),
1096+
TestContext.CancellationToken);
1097+
1098+
// Assert
1099+
Assert.IsFalse(result);
1100+
}
1101+
1102+
#endregion
10191103
}

src/winapp-CLI/WinApp.Cli.Tests/FakeDotNetService.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,7 @@ public Task<bool> RemoveWindowsPackageTypeNoneAsync(FileInfo csprojPath, Cancell
7676

7777
public Task<bool> AnnotatePackageReferencesAsync(FileInfo csprojPath, IReadOnlyDictionary<string, string> packageComments, CancellationToken cancellationToken = default)
7878
=> _real.AnnotatePackageReferencesAsync(csprojPath, packageComments, cancellationToken);
79+
80+
public Task<bool> EnsureAssetContentItemsAsync(FileInfo csprojPath, CancellationToken cancellationToken = default)
81+
=> _real.EnsureAssetContentItemsAsync(csprojPath, cancellationToken);
7982
}

src/winapp-CLI/WinApp.Cli/Services/DotNetService.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,41 @@ public async Task<bool> AnnotatePackageReferencesAsync(FileInfo csprojPath, IRea
631631

632632
return modified;
633633
}
634+
635+
public async Task<bool> EnsureAssetContentItemsAsync(FileInfo csprojPath, CancellationToken cancellationToken = default)
636+
{
637+
if (!csprojPath.Exists)
638+
{
639+
return false;
640+
}
641+
642+
var content = await File.ReadAllTextAsync(csprojPath.FullName, cancellationToken);
643+
644+
// Skip if the csproj already includes Assets content (glob or individual entries)
645+
if (AssetsContentItemRegex().IsMatch(content))
646+
{
647+
return false;
648+
}
649+
650+
// Insert a new ItemGroup with the Assets glob before </Project>
651+
var closeProjectIdx = content.LastIndexOf("</Project>", StringComparison.OrdinalIgnoreCase);
652+
if (closeProjectIdx < 0)
653+
{
654+
return false;
655+
}
656+
657+
var itemGroup =
658+
" <ItemGroup>" + Environment.NewLine
659+
+ " <Content Include=\"Assets\\**\" />" + Environment.NewLine
660+
+ " </ItemGroup>" + Environment.NewLine + Environment.NewLine;
661+
662+
content = content[..closeProjectIdx] + itemGroup + content[closeProjectIdx..];
663+
await File.WriteAllTextAsync(csprojPath.FullName, content, cancellationToken);
664+
return true;
665+
}
666+
667+
[GeneratedRegex(@"<Content\s[^>]*Include\s*=\s*""Assets\\", RegexOptions.IgnoreCase)]
668+
private static partial Regex AssetsContentItemRegex();
634669
}
635670

636671
[JsonSerializable(typeof(DotNetPackageListJson))]

src/winapp-CLI/WinApp.Cli/Services/IDotNetService.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,14 @@ internal interface IDotNetService
121121
/// <param name="cancellationToken">Cancellation token.</param>
122122
/// <returns>True if any comments were added, false if all packages already had comments or were not found.</returns>
123123
Task<bool> AnnotatePackageReferencesAsync(FileInfo csprojPath, IReadOnlyDictionary<string, string> packageComments, CancellationToken cancellationToken = default);
124+
125+
/// <summary>
126+
/// Ensures the .csproj contains a <c>&lt;Content Include="Assets\**" /&gt;</c> item so that
127+
/// generated visual assets (StoreLogo, AppList, etc.) are included in the MSIX package layout.
128+
/// Without this, non-WinUI projects exclude the assets from the .build.appxrecipe.
129+
/// </summary>
130+
/// <param name="csprojPath">The project file to update.</param>
131+
/// <param name="cancellationToken">Cancellation token.</param>
132+
/// <returns>True if the .csproj was modified, false if it already had asset content items.</returns>
133+
Task<bool> EnsureAssetContentItemsAsync(FileInfo csprojPath, CancellationToken cancellationToken = default);
124134
}

src/winapp-CLI/WinApp.Cli/Services/WorkspaceSetupService.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,15 @@ await taskContext.AddSubTaskAsync("Installing Windows App SDK Runtime", async (t
646646
await SetupManifestSubTaskAsync(options, shouldGenerateManifest, manifestGenerationInfo, taskContext, cancellationToken);
647647
}
648648

649+
// Add generated assets as Content items so MSIX tooling includes them in the package layout
650+
if (isDotNetProject && csprojFile != null && shouldGenerateManifest)
651+
{
652+
if (await dotNetService.EnsureAssetContentItemsAsync(csprojFile, cancellationToken))
653+
{
654+
taskContext.AddDebugMessage($"{UiSymbols.Check} Added asset Content items to .csproj");
655+
}
656+
}
657+
649658
// Save configuration (native/C++ projects only — .NET uses .csproj PackageReferences)
650659
if (!isDotNetProject && !options.RequireExistingConfig && options.SdkInstallMode != SdkInstallMode.None && usedVersions != null)
651660
{

0 commit comments

Comments
 (0)