Skip to content

Commit 48da9ae

Browse files
authored
feat(sdk): auto-inject Content entries into vsixmanifest (#42)
* feat(sdk): auto-inject Content entries into vsixmanifest Automatically inject <Content><ProjectTemplate/> and <Content><ItemTemplate/> entries into the vsixmanifest when templates are discovered, eliminating the need for manual manifest edits. - Add custom MSBuild task (InjectVsixManifestContentTask) that properly parses and modifies the XML manifest - Write to intermediate manifest (obj folder) to avoid modifying source - Enable by default via AutoInjectVsixTemplateContent property - Skip existing WarnMissingManifestContent warnings when auto-injection is enabled - Add E2E test to verify auto-injection works correctly Closes #40 * docs(templates): document auto-inject Content feature
1 parent 410bde4 commit 48da9ae

14 files changed

Lines changed: 400 additions & 17 deletions

File tree

.github/workflows/build.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ jobs:
8787
- name: Build E2E.AllFeatures
8888
run: dotnet build tests/e2e/E2E.AllFeatures/E2E.AllFeatures.csproj -c Release
8989

90+
- name: Build E2E.Templates.AutoInject
91+
run: dotnet build tests/e2e/E2E.Templates.AutoInject/E2E.Templates.AutoInject.csproj -c Release
92+
9093
# VSIX Verification - Check that VSIX files contain expected content
9194
- name: Verify E2E.Minimal VSIX
9295
run: |
@@ -141,6 +144,16 @@ jobs:
141144
if ($files -notcontains "E2E.AllFeatures.dll") { throw "Missing E2E.AllFeatures.dll" }
142145
Write-Host "E2E.AllFeatures VSIX verified successfully"
143146
147+
- name: Verify E2E.Templates.AutoInject VSIX
148+
run: |
149+
$vsix = "tests/e2e/E2E.Templates.AutoInject/bin/Release/net472/E2E.Templates.AutoInject.vsix"
150+
if (!(Test-Path $vsix)) { throw "VSIX not found: $vsix" }
151+
Expand-Archive -Path $vsix -DestinationPath "tests/e2e/E2E.Templates.AutoInject/vsix-contents" -Force
152+
$files = Get-ChildItem -Path "tests/e2e/E2E.Templates.AutoInject/vsix-contents" -Recurse | Select-Object -ExpandProperty Name
153+
# Verify template was included (Content was auto-injected)
154+
if ($files -notcontains "TestProject.vstemplate") { throw "Missing ProjectTemplates/TestProject/TestProject.vstemplate" }
155+
Write-Host "E2E.Templates.AutoInject VSIX verified successfully"
156+
144157
- name: Test Template - Install
145158
run: dotnet new install artifacts/packages/CodingWithCalvin.VsixSdk.Templates.1.0.0.nupkg
146159

docs/templates.md

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ This SDK handles template packaging by:
1717

1818
1. **Auto-discovering** templates in `ProjectTemplates/` and `ItemTemplates/` folders
1919
2. **Including template files** in the VSIX package automatically
20-
3. **Supporting cross-project template references** for including templates from other SDK-style projects
21-
4. **Providing validation warnings** if your manifest is missing required Content entries
20+
3. **Auto-injecting manifest Content entries** so you don't need to manually edit the manifest
21+
4. **Supporting cross-project template references** for including templates from other SDK-style projects
2222

23-
Your manifest must contain `<Content><ProjectTemplate/></Content>` entries for Visual Studio to register and display templates. The SDK includes the template files in the VSIX; the manifest entries tell VS how to find and register them.
23+
Visual Studio requires `<Content><ProjectTemplate/></Content>` entries in the manifest to register and display templates. The SDK automatically injects these entries when templates are discovered, so you don't need to add them manually.
2424

2525
## Item Types
2626

@@ -56,12 +56,10 @@ MyExtension/
5656
MyClass.cs
5757
```
5858

59-
With this structure, minimal configuration is needed. The SDK will:
59+
With this structure, no additional configuration is needed. The SDK will:
6060
1. Find the templates automatically
6161
2. Include all template files in the VSIX
62-
3. Warn if `<Content>` entries are missing from the manifest
63-
64-
You need to add the Content entries to your manifest manually (see Manifest Configuration below).
62+
3. Inject the required `<Content>` entries into the manifest automatically
6563

6664
### Disabling Auto-Discovery
6765

@@ -104,7 +102,29 @@ The `TemplatePath` is relative to the referenced project's directory.
104102

105103
## Manifest Configuration
106104

107-
Visual Studio requires `<Content>` entries in your `.vsixmanifest` to register templates. Add these to your manifest:
105+
Visual Studio requires `<Content>` entries in your `.vsixmanifest` to register templates. **The SDK automatically injects these entries** when templates are discovered, so you typically don't need to add them manually.
106+
107+
### Automatic Content Injection (Default)
108+
109+
When `AutoInjectVsixTemplateContent` is enabled (the default), the SDK:
110+
1. Creates an intermediate manifest in the `obj` folder
111+
2. Injects `<ProjectTemplate Path="ProjectTemplates"/>` if project templates are discovered
112+
3. Injects `<ItemTemplate Path="ItemTemplates"/>` if item templates are discovered
113+
4. Uses the intermediate manifest for VSIX packaging (your source manifest is never modified)
114+
115+
This means your source `.vsixmanifest` does not need a `<Content>` section at all when using templates.
116+
117+
### Disabling Auto-Injection
118+
119+
If you prefer to manage the manifest Content entries manually, disable auto-injection:
120+
121+
```xml
122+
<PropertyGroup>
123+
<AutoInjectVsixTemplateContent>false</AutoInjectVsixTemplateContent>
124+
</PropertyGroup>
125+
```
126+
127+
When auto-injection is disabled, you must add the Content entries to your manifest manually:
108128

109129
```xml
110130
<Content>
@@ -117,6 +137,8 @@ The SDK will emit warnings if you have templates but missing manifest entries:
117137
- **VSIXSDK011**: Project templates defined but no `<ProjectTemplate>` in manifest
118138
- **VSIXSDK012**: Item templates defined but no `<ItemTemplate>` in manifest
119139

140+
These warnings only appear when `AutoInjectVsixTemplateContent` is set to `false`.
141+
120142
## Complete Example
121143

122144
### Project File
@@ -142,7 +164,7 @@ The SDK will emit warnings if you have templates but missing manifest entries:
142164

143165
### Manifest File
144166

145-
Add the `<Content>` entries for your templates:
167+
The SDK automatically injects Content entries, so your manifest doesn't need them:
146168

147169
```xml
148170
<?xml version="1.0" encoding="utf-8"?>
@@ -157,10 +179,7 @@ Add the `<Content>` entries for your templates:
157179
<ProductArchitecture>amd64</ProductArchitecture>
158180
</InstallationTarget>
159181
</Installation>
160-
<Content>
161-
<ProjectTemplate Path="ProjectTemplates" />
162-
<ItemTemplate Path="ItemTemplates" />
163-
</Content>
182+
<!-- Content entries are auto-injected by the SDK -->
164183
</PackageManifest>
165184
```
166185

@@ -198,10 +217,11 @@ Add the `<Content>` entries for your templates:
198217

199218
### Templates not appearing in Visual Studio
200219

201-
1. Ensure your manifest has the appropriate `<Content>` entries
202-
2. Check that the template folders are included in the VSIX (open the .vsix as a zip)
220+
1. Check that the template folders are included in the VSIX (open the .vsix as a zip)
221+
2. Verify the intermediate manifest (`obj/*/source.extension.vsixmanifest`) contains the `<Content>` entries
203222
3. Verify the `.vstemplate` file has correct `<ProjectType>` or `<TemplateGroupID>`
204223
4. Reset the Visual Studio template cache: delete `%LocalAppData%\Microsoft\VisualStudio\<version>\ComponentModelCache`
224+
5. If using `AutoInjectVsixTemplateContent=false`, ensure your source manifest has the `<Content>` entries
205225

206226
### Build errors about missing template folders
207227

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net472</TargetFramework>
5+
<LangVersion>latest</LangVersion>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Microsoft.Build.Framework" Version="17.3.2" ExcludeAssets="runtime" />
12+
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.3.2" ExcludeAssets="runtime" />
13+
</ItemGroup>
14+
15+
</Project>
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
using System;
2+
using System.IO;
3+
using System.Xml;
4+
using Microsoft.Build.Framework;
5+
using Microsoft.Build.Utilities;
6+
7+
namespace CodingWithCalvin.VsixSdk.Tasks;
8+
9+
/// <summary>
10+
/// MSBuild task that injects Content entries into a VSIX manifest for discovered templates.
11+
/// Creates an intermediate manifest file without modifying the source.
12+
/// </summary>
13+
public class InjectVsixManifestContentTask : Task
14+
{
15+
private const string VsixNamespace = "http://schemas.microsoft.com/developer/vsx-schema/2011";
16+
17+
/// <summary>
18+
/// Path to the source VSIX manifest file.
19+
/// </summary>
20+
[Required]
21+
public string SourceManifestPath { get; set; } = string.Empty;
22+
23+
/// <summary>
24+
/// Path where the modified manifest will be written.
25+
/// </summary>
26+
[Required]
27+
public string OutputManifestPath { get; set; } = string.Empty;
28+
29+
/// <summary>
30+
/// Whether project templates were discovered.
31+
/// </summary>
32+
public bool HasProjectTemplates { get; set; }
33+
34+
/// <summary>
35+
/// Whether item templates were discovered.
36+
/// </summary>
37+
public bool HasItemTemplates { get; set; }
38+
39+
/// <summary>
40+
/// The folder path for project templates (default: "ProjectTemplates").
41+
/// </summary>
42+
public string ProjectTemplatesPath { get; set; } = "ProjectTemplates";
43+
44+
/// <summary>
45+
/// The folder path for item templates (default: "ItemTemplates").
46+
/// </summary>
47+
public string ItemTemplatesPath { get; set; } = "ItemTemplates";
48+
49+
public override bool Execute()
50+
{
51+
try
52+
{
53+
if (!File.Exists(SourceManifestPath))
54+
{
55+
Log.LogError("VSIXSDK020", null, null, null, 0, 0, 0, 0,
56+
"Source manifest not found: {0}", SourceManifestPath);
57+
return false;
58+
}
59+
60+
var doc = new XmlDocument();
61+
doc.PreserveWhitespace = true;
62+
doc.Load(SourceManifestPath);
63+
64+
var nsmgr = new XmlNamespaceManager(doc.NameTable);
65+
nsmgr.AddNamespace("vsix", VsixNamespace);
66+
67+
var modified = false;
68+
69+
var packageManifest = doc.SelectSingleNode("/vsix:PackageManifest", nsmgr);
70+
if (packageManifest == null)
71+
{
72+
Log.LogError("VSIXSDK021", null, null, SourceManifestPath, 0, 0, 0, 0,
73+
"Invalid manifest: PackageManifest element not found");
74+
return false;
75+
}
76+
77+
var contentElement = doc.SelectSingleNode("/vsix:PackageManifest/vsix:Content", nsmgr);
78+
if (contentElement == null && (HasProjectTemplates || HasItemTemplates))
79+
{
80+
contentElement = doc.CreateElement("Content", VsixNamespace);
81+
packageManifest.AppendChild(contentElement);
82+
modified = true;
83+
Log.LogMessage(MessageImportance.Normal, "Created Content element in manifest");
84+
}
85+
86+
if (contentElement != null)
87+
{
88+
if (HasProjectTemplates)
89+
{
90+
var existingProjectTemplate = contentElement.SelectSingleNode(
91+
"vsix:ProjectTemplate", nsmgr);
92+
if (existingProjectTemplate == null)
93+
{
94+
var projectTemplateElement = doc.CreateElement("ProjectTemplate", VsixNamespace);
95+
projectTemplateElement.SetAttribute("Path", ProjectTemplatesPath);
96+
contentElement.AppendChild(projectTemplateElement);
97+
modified = true;
98+
Log.LogMessage(MessageImportance.Normal,
99+
"Added ProjectTemplate entry with Path='{0}'", ProjectTemplatesPath);
100+
}
101+
else
102+
{
103+
Log.LogMessage(MessageImportance.Low,
104+
"ProjectTemplate entry already exists, skipping injection");
105+
}
106+
}
107+
108+
if (HasItemTemplates)
109+
{
110+
var existingItemTemplate = contentElement.SelectSingleNode(
111+
"vsix:ItemTemplate", nsmgr);
112+
if (existingItemTemplate == null)
113+
{
114+
var itemTemplateElement = doc.CreateElement("ItemTemplate", VsixNamespace);
115+
itemTemplateElement.SetAttribute("Path", ItemTemplatesPath);
116+
contentElement.AppendChild(itemTemplateElement);
117+
modified = true;
118+
Log.LogMessage(MessageImportance.Normal,
119+
"Added ItemTemplate entry with Path='{0}'", ItemTemplatesPath);
120+
}
121+
else
122+
{
123+
Log.LogMessage(MessageImportance.Low,
124+
"ItemTemplate entry already exists, skipping injection");
125+
}
126+
}
127+
}
128+
129+
var outputDir = Path.GetDirectoryName(OutputManifestPath);
130+
if (!string.IsNullOrEmpty(outputDir) && !Directory.Exists(outputDir))
131+
{
132+
Directory.CreateDirectory(outputDir);
133+
}
134+
135+
doc.Save(OutputManifestPath);
136+
137+
if (modified)
138+
{
139+
Log.LogMessage(MessageImportance.High,
140+
"Injected template Content entries into manifest: {0}", OutputManifestPath);
141+
}
142+
else
143+
{
144+
Log.LogMessage(MessageImportance.Normal,
145+
"No template Content injection needed, copied manifest to: {0}", OutputManifestPath);
146+
}
147+
148+
return true;
149+
}
150+
catch (Exception ex)
151+
{
152+
Log.LogErrorFromException(ex, showStackTrace: true);
153+
return false;
154+
}
155+
}
156+
}

src/CodingWithCalvin.VsixSdk/CodingWithCalvin.VsixSdk.csproj

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@
4343
</ProjectReference>
4444
</ItemGroup>
4545

46+
<!-- Reference the tasks project -->
47+
<ItemGroup>
48+
<ProjectReference Include="..\CodingWithCalvin.VsixSdk.Tasks\CodingWithCalvin.VsixSdk.Tasks.csproj">
49+
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
50+
<SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
51+
<PrivateAssets>all</PrivateAssets>
52+
</ProjectReference>
53+
</ItemGroup>
54+
4655
<!-- Include the SDK files in the package at the correct location -->
4756
<ItemGroup>
4857
<!-- The Sdk folder must be at the root of the package -->
@@ -54,6 +63,12 @@
5463
PackagePath="analyzers\dotnet\cs"
5564
Visible="false" />
5665

66+
<!-- Include the MSBuild tasks -->
67+
<None Include="..\CodingWithCalvin.VsixSdk.Tasks\bin\$(Configuration)\net472\CodingWithCalvin.VsixSdk.Tasks.dll"
68+
Pack="true"
69+
PackagePath="build"
70+
Visible="false" />
71+
5772
<!-- Include README at package root -->
5873
<None Include="..\..\README.md" Pack="true" PackagePath="" />
5974
</ItemGroup>

src/CodingWithCalvin.VsixSdk/Sdk/Sdk.Vsix.Templates.props

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
<!-- Enable/disable automatic template discovery -->
1919
<EnableDefaultVsixTemplateItems Condition="'$(EnableDefaultVsixTemplateItems)' == ''">true</EnableDefaultVsixTemplateItems>
2020

21+
<!-- Enable/disable automatic Content entry injection for discovered templates -->
22+
<AutoInjectVsixTemplateContent Condition="'$(AutoInjectVsixTemplateContent)' == ''">true</AutoInjectVsixTemplateContent>
23+
2124
<!-- Default folders for template discovery -->
2225
<VsixProjectTemplatesFolder Condition="'$(VsixProjectTemplatesFolder)' == ''">ProjectTemplates</VsixProjectTemplatesFolder>
2326
<VsixItemTemplatesFolder Condition="'$(VsixItemTemplatesFolder)' == ''">ItemTemplates</VsixItemTemplatesFolder>

0 commit comments

Comments
 (0)