Skip to content

Commit 76dffe2

Browse files
zateutschCopilotCopilotnmetulev
authored
Add assets to .csproj during win app init (#461)
fixes #444 This pull request makes it so that all files in the Assets directory are added as content items to the csproj during `winapp init`. Uses a glob pattern `Assets/**` instead of individual references. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Nikola Metulev <nmetulev@users.noreply.github.com>
1 parent 3e20de9 commit 76dffe2

5 files changed

Lines changed: 141 additions & 0 deletions

File tree

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,4 +1062,88 @@ await File.WriteAllTextAsync(csprojPath, """
10621062
}
10631063

10641064
#endregion
1065+
1066+
#region EnsureAssetContentItemsAsync
1067+
1068+
[TestMethod]
1069+
public async Task EnsureAssetContentItems_AddsMissingAssetsGlob()
1070+
{
1071+
// Arrange
1072+
var csprojPath = Path.Combine(_testTempDirectory, "Test.csproj");
1073+
File.WriteAllText(csprojPath, @"<Project Sdk=""Microsoft.NET.Sdk"">
1074+
<PropertyGroup>
1075+
<TargetFramework>net10.0</TargetFramework>
1076+
</PropertyGroup>
1077+
</Project>
1078+
");
1079+
1080+
// Act
1081+
var result = await _dotNetService.EnsureAssetContentItemsAsync(
1082+
new FileInfo(csprojPath), TestContext.CancellationToken);
1083+
1084+
// Assert
1085+
Assert.IsTrue(result, "Should modify csproj when no asset Content items exist");
1086+
var content = File.ReadAllText(csprojPath);
1087+
Assert.IsTrue(content.Contains(@"<Content Include=""Assets\**\*"" />"),
1088+
"Should add Assets glob Content item");
1089+
Assert.IsTrue(content.Contains("</Project>"),
1090+
"Should preserve </Project> closing tag");
1091+
}
1092+
1093+
[TestMethod]
1094+
public async Task EnsureAssetContentItems_SkipsWhenAlreadyPresent()
1095+
{
1096+
// Arrange
1097+
var csprojPath = Path.Combine(_testTempDirectory, "Test.csproj");
1098+
File.WriteAllText(csprojPath, @"<Project Sdk=""Microsoft.NET.Sdk"">
1099+
<PropertyGroup>
1100+
<TargetFramework>net10.0</TargetFramework>
1101+
</PropertyGroup>
1102+
<ItemGroup>
1103+
<Content Include=""Assets\**\*"" />
1104+
</ItemGroup>
1105+
</Project>
1106+
");
1107+
1108+
// Act
1109+
var result = await _dotNetService.EnsureAssetContentItemsAsync(
1110+
new FileInfo(csprojPath), TestContext.CancellationToken);
1111+
1112+
// Assert
1113+
Assert.IsFalse(result, "Should not modify csproj when asset Content items already exist");
1114+
}
1115+
1116+
[TestMethod]
1117+
public async Task EnsureAssetContentItems_SkipsWhenIndividualAssetPresent()
1118+
{
1119+
// Arrange
1120+
var csprojPath = Path.Combine(_testTempDirectory, "Test.csproj");
1121+
File.WriteAllText(csprojPath, @"<Project Sdk=""Microsoft.NET.Sdk"">
1122+
<ItemGroup>
1123+
<Content Include=""Assets\StoreLogo.png"" />
1124+
</ItemGroup>
1125+
</Project>
1126+
");
1127+
1128+
// Act
1129+
var result = await _dotNetService.EnsureAssetContentItemsAsync(
1130+
new FileInfo(csprojPath), TestContext.CancellationToken);
1131+
1132+
// Assert
1133+
Assert.IsFalse(result, "Should not modify csproj when individual asset Content items exist");
1134+
}
1135+
1136+
[TestMethod]
1137+
public async Task EnsureAssetContentItems_ReturnsFalseForMissingFile()
1138+
{
1139+
// Act
1140+
var result = await _dotNetService.EnsureAssetContentItemsAsync(
1141+
new FileInfo(Path.Combine(_testTempDirectory, "NonExistent.csproj")),
1142+
TestContext.CancellationToken);
1143+
1144+
// Assert
1145+
Assert.IsFalse(result);
1146+
}
1147+
1148+
#endregion
10651149
}

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
@@ -675,6 +675,41 @@ public async Task<bool> AnnotatePackageReferencesAsync(FileInfo csprojPath, IRea
675675

676676
return modified;
677677
}
678+
679+
public async Task<bool> EnsureAssetContentItemsAsync(FileInfo csprojPath, CancellationToken cancellationToken = default)
680+
{
681+
if (!csprojPath.Exists)
682+
{
683+
return false;
684+
}
685+
686+
var content = await File.ReadAllTextAsync(csprojPath.FullName, cancellationToken);
687+
688+
// Skip if the csproj already includes Assets content (glob or individual entries)
689+
if (AssetsContentItemRegex().IsMatch(content))
690+
{
691+
return false;
692+
}
693+
694+
// Insert a new ItemGroup with the Assets glob before </Project>
695+
var closeProjectIdx = content.LastIndexOf("</Project>", StringComparison.OrdinalIgnoreCase);
696+
if (closeProjectIdx < 0)
697+
{
698+
return false;
699+
}
700+
701+
var itemGroup =
702+
" <ItemGroup>" + Environment.NewLine
703+
+ " <Content Include=\"Assets\\**\\*\" />" + Environment.NewLine
704+
+ " </ItemGroup>" + Environment.NewLine + Environment.NewLine;
705+
706+
content = content[..closeProjectIdx] + itemGroup + content[closeProjectIdx..];
707+
await File.WriteAllTextAsync(csprojPath.FullName, content, cancellationToken);
708+
return true;
709+
}
710+
711+
[GeneratedRegex(@"<Content\s[^>]*Include\s*=\s*""Assets\\", RegexOptions.IgnoreCase)]
712+
private static partial Regex AssetsContentItemRegex();
678713
}
679714

680715
[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
@@ -652,6 +652,15 @@ await taskContext.AddSubTaskAsync("Installing Windows App SDK Runtime", async (t
652652
await SetupManifestSubTaskAsync(options, shouldGenerateManifest, manifestGenerationInfo, taskContext, cancellationToken);
653653
}
654654

655+
// Add generated assets as Content items so MSIX tooling includes them in the package layout
656+
if (isDotNetProject && csprojFile != null && shouldGenerateManifest)
657+
{
658+
if (await dotNetService.EnsureAssetContentItemsAsync(csprojFile, cancellationToken))
659+
{
660+
taskContext.AddDebugMessage($"{UiSymbols.Check} Added asset Content items to .csproj");
661+
}
662+
}
663+
655664
// Save configuration (native/C++ projects only — .NET uses .csproj PackageReferences)
656665
if (!isDotNetProject && !options.RequireExistingConfig && options.SdkInstallMode != SdkInstallMode.None && usedVersions != null)
657666
{

0 commit comments

Comments
 (0)