diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index d75cbf9..f0544b2 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -87,6 +87,9 @@ jobs:
- name: Build E2E.AllFeatures
run: dotnet build tests/e2e/E2E.AllFeatures/E2E.AllFeatures.csproj -c Release
+ - name: Build E2E.Templates.AutoInject
+ run: dotnet build tests/e2e/E2E.Templates.AutoInject/E2E.Templates.AutoInject.csproj -c Release
+
# VSIX Verification - Check that VSIX files contain expected content
- name: Verify E2E.Minimal VSIX
run: |
@@ -141,6 +144,16 @@ jobs:
if ($files -notcontains "E2E.AllFeatures.dll") { throw "Missing E2E.AllFeatures.dll" }
Write-Host "E2E.AllFeatures VSIX verified successfully"
+ - name: Verify E2E.Templates.AutoInject VSIX
+ run: |
+ $vsix = "tests/e2e/E2E.Templates.AutoInject/bin/Release/net472/E2E.Templates.AutoInject.vsix"
+ if (!(Test-Path $vsix)) { throw "VSIX not found: $vsix" }
+ Expand-Archive -Path $vsix -DestinationPath "tests/e2e/E2E.Templates.AutoInject/vsix-contents" -Force
+ $files = Get-ChildItem -Path "tests/e2e/E2E.Templates.AutoInject/vsix-contents" -Recurse | Select-Object -ExpandProperty Name
+ # Verify template was included (Content was auto-injected)
+ if ($files -notcontains "TestProject.vstemplate") { throw "Missing ProjectTemplates/TestProject/TestProject.vstemplate" }
+ Write-Host "E2E.Templates.AutoInject VSIX verified successfully"
+
- name: Test Template - Install
run: dotnet new install artifacts/packages/CodingWithCalvin.VsixSdk.Templates.1.0.0.nupkg
diff --git a/docs/templates.md b/docs/templates.md
index 598a96e..33f82ad 100644
--- a/docs/templates.md
+++ b/docs/templates.md
@@ -17,10 +17,10 @@ This SDK handles template packaging by:
1. **Auto-discovering** templates in `ProjectTemplates/` and `ItemTemplates/` folders
2. **Including template files** in the VSIX package automatically
-3. **Supporting cross-project template references** for including templates from other SDK-style projects
-4. **Providing validation warnings** if your manifest is missing required Content entries
+3. **Auto-injecting manifest Content entries** so you don't need to manually edit the manifest
+4. **Supporting cross-project template references** for including templates from other SDK-style projects
-Your manifest must contain `` 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.
+Visual Studio requires `` 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.
## Item Types
@@ -56,12 +56,10 @@ MyExtension/
MyClass.cs
```
-With this structure, minimal configuration is needed. The SDK will:
+With this structure, no additional configuration is needed. The SDK will:
1. Find the templates automatically
2. Include all template files in the VSIX
-3. Warn if `` entries are missing from the manifest
-
-You need to add the Content entries to your manifest manually (see Manifest Configuration below).
+3. Inject the required `` entries into the manifest automatically
### Disabling Auto-Discovery
@@ -104,7 +102,29 @@ The `TemplatePath` is relative to the referenced project's directory.
## Manifest Configuration
-Visual Studio requires `` entries in your `.vsixmanifest` to register templates. Add these to your manifest:
+Visual Studio requires `` 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.
+
+### Automatic Content Injection (Default)
+
+When `AutoInjectVsixTemplateContent` is enabled (the default), the SDK:
+1. Creates an intermediate manifest in the `obj` folder
+2. Injects `` if project templates are discovered
+3. Injects `` if item templates are discovered
+4. Uses the intermediate manifest for VSIX packaging (your source manifest is never modified)
+
+This means your source `.vsixmanifest` does not need a `` section at all when using templates.
+
+### Disabling Auto-Injection
+
+If you prefer to manage the manifest Content entries manually, disable auto-injection:
+
+```xml
+
+ false
+
+```
+
+When auto-injection is disabled, you must add the Content entries to your manifest manually:
```xml
@@ -117,6 +137,8 @@ The SDK will emit warnings if you have templates but missing manifest entries:
- **VSIXSDK011**: Project templates defined but no `` in manifest
- **VSIXSDK012**: Item templates defined but no `` in manifest
+These warnings only appear when `AutoInjectVsixTemplateContent` is set to `false`.
+
## Complete Example
### Project File
@@ -142,7 +164,7 @@ The SDK will emit warnings if you have templates but missing manifest entries:
### Manifest File
-Add the `` entries for your templates:
+The SDK automatically injects Content entries, so your manifest doesn't need them:
```xml
@@ -157,10 +179,7 @@ Add the `` entries for your templates:
amd64
-
-
-
-
+
```
@@ -198,10 +217,11 @@ Add the `` entries for your templates:
### Templates not appearing in Visual Studio
-1. Ensure your manifest has the appropriate `` entries
-2. Check that the template folders are included in the VSIX (open the .vsix as a zip)
+1. Check that the template folders are included in the VSIX (open the .vsix as a zip)
+2. Verify the intermediate manifest (`obj/*/source.extension.vsixmanifest`) contains the `` entries
3. Verify the `.vstemplate` file has correct `` or ``
4. Reset the Visual Studio template cache: delete `%LocalAppData%\Microsoft\VisualStudio\\ComponentModelCache`
+5. If using `AutoInjectVsixTemplateContent=false`, ensure your source manifest has the `` entries
### Build errors about missing template folders
diff --git a/src/CodingWithCalvin.VsixSdk.Tasks/CodingWithCalvin.VsixSdk.Tasks.csproj b/src/CodingWithCalvin.VsixSdk.Tasks/CodingWithCalvin.VsixSdk.Tasks.csproj
new file mode 100644
index 0000000..3644843
--- /dev/null
+++ b/src/CodingWithCalvin.VsixSdk.Tasks/CodingWithCalvin.VsixSdk.Tasks.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net472
+ latest
+ enable
+ false
+
+
+
+
+
+
+
+
diff --git a/src/CodingWithCalvin.VsixSdk.Tasks/InjectVsixManifestContentTask.cs b/src/CodingWithCalvin.VsixSdk.Tasks/InjectVsixManifestContentTask.cs
new file mode 100644
index 0000000..987867c
--- /dev/null
+++ b/src/CodingWithCalvin.VsixSdk.Tasks/InjectVsixManifestContentTask.cs
@@ -0,0 +1,156 @@
+using System;
+using System.IO;
+using System.Xml;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace CodingWithCalvin.VsixSdk.Tasks;
+
+///
+/// MSBuild task that injects Content entries into a VSIX manifest for discovered templates.
+/// Creates an intermediate manifest file without modifying the source.
+///
+public class InjectVsixManifestContentTask : Task
+{
+ private const string VsixNamespace = "http://schemas.microsoft.com/developer/vsx-schema/2011";
+
+ ///
+ /// Path to the source VSIX manifest file.
+ ///
+ [Required]
+ public string SourceManifestPath { get; set; } = string.Empty;
+
+ ///
+ /// Path where the modified manifest will be written.
+ ///
+ [Required]
+ public string OutputManifestPath { get; set; } = string.Empty;
+
+ ///
+ /// Whether project templates were discovered.
+ ///
+ public bool HasProjectTemplates { get; set; }
+
+ ///
+ /// Whether item templates were discovered.
+ ///
+ public bool HasItemTemplates { get; set; }
+
+ ///
+ /// The folder path for project templates (default: "ProjectTemplates").
+ ///
+ public string ProjectTemplatesPath { get; set; } = "ProjectTemplates";
+
+ ///
+ /// The folder path for item templates (default: "ItemTemplates").
+ ///
+ public string ItemTemplatesPath { get; set; } = "ItemTemplates";
+
+ public override bool Execute()
+ {
+ try
+ {
+ if (!File.Exists(SourceManifestPath))
+ {
+ Log.LogError("VSIXSDK020", null, null, null, 0, 0, 0, 0,
+ "Source manifest not found: {0}", SourceManifestPath);
+ return false;
+ }
+
+ var doc = new XmlDocument();
+ doc.PreserveWhitespace = true;
+ doc.Load(SourceManifestPath);
+
+ var nsmgr = new XmlNamespaceManager(doc.NameTable);
+ nsmgr.AddNamespace("vsix", VsixNamespace);
+
+ var modified = false;
+
+ var packageManifest = doc.SelectSingleNode("/vsix:PackageManifest", nsmgr);
+ if (packageManifest == null)
+ {
+ Log.LogError("VSIXSDK021", null, null, SourceManifestPath, 0, 0, 0, 0,
+ "Invalid manifest: PackageManifest element not found");
+ return false;
+ }
+
+ var contentElement = doc.SelectSingleNode("/vsix:PackageManifest/vsix:Content", nsmgr);
+ if (contentElement == null && (HasProjectTemplates || HasItemTemplates))
+ {
+ contentElement = doc.CreateElement("Content", VsixNamespace);
+ packageManifest.AppendChild(contentElement);
+ modified = true;
+ Log.LogMessage(MessageImportance.Normal, "Created Content element in manifest");
+ }
+
+ if (contentElement != null)
+ {
+ if (HasProjectTemplates)
+ {
+ var existingProjectTemplate = contentElement.SelectSingleNode(
+ "vsix:ProjectTemplate", nsmgr);
+ if (existingProjectTemplate == null)
+ {
+ var projectTemplateElement = doc.CreateElement("ProjectTemplate", VsixNamespace);
+ projectTemplateElement.SetAttribute("Path", ProjectTemplatesPath);
+ contentElement.AppendChild(projectTemplateElement);
+ modified = true;
+ Log.LogMessage(MessageImportance.Normal,
+ "Added ProjectTemplate entry with Path='{0}'", ProjectTemplatesPath);
+ }
+ else
+ {
+ Log.LogMessage(MessageImportance.Low,
+ "ProjectTemplate entry already exists, skipping injection");
+ }
+ }
+
+ if (HasItemTemplates)
+ {
+ var existingItemTemplate = contentElement.SelectSingleNode(
+ "vsix:ItemTemplate", nsmgr);
+ if (existingItemTemplate == null)
+ {
+ var itemTemplateElement = doc.CreateElement("ItemTemplate", VsixNamespace);
+ itemTemplateElement.SetAttribute("Path", ItemTemplatesPath);
+ contentElement.AppendChild(itemTemplateElement);
+ modified = true;
+ Log.LogMessage(MessageImportance.Normal,
+ "Added ItemTemplate entry with Path='{0}'", ItemTemplatesPath);
+ }
+ else
+ {
+ Log.LogMessage(MessageImportance.Low,
+ "ItemTemplate entry already exists, skipping injection");
+ }
+ }
+ }
+
+ var outputDir = Path.GetDirectoryName(OutputManifestPath);
+ if (!string.IsNullOrEmpty(outputDir) && !Directory.Exists(outputDir))
+ {
+ Directory.CreateDirectory(outputDir);
+ }
+
+ doc.Save(OutputManifestPath);
+
+ if (modified)
+ {
+ Log.LogMessage(MessageImportance.High,
+ "Injected template Content entries into manifest: {0}", OutputManifestPath);
+ }
+ else
+ {
+ Log.LogMessage(MessageImportance.Normal,
+ "No template Content injection needed, copied manifest to: {0}", OutputManifestPath);
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Log.LogErrorFromException(ex, showStackTrace: true);
+ return false;
+ }
+ }
+}
diff --git a/src/CodingWithCalvin.VsixSdk/CodingWithCalvin.VsixSdk.csproj b/src/CodingWithCalvin.VsixSdk/CodingWithCalvin.VsixSdk.csproj
index f2b8353..288abed 100644
--- a/src/CodingWithCalvin.VsixSdk/CodingWithCalvin.VsixSdk.csproj
+++ b/src/CodingWithCalvin.VsixSdk/CodingWithCalvin.VsixSdk.csproj
@@ -43,6 +43,15 @@
+
+
+
+ false
+ true
+ all
+
+
+
@@ -54,6 +63,12 @@
PackagePath="analyzers\dotnet\cs"
Visible="false" />
+
+
+
diff --git a/src/CodingWithCalvin.VsixSdk/Sdk/Sdk.Vsix.Templates.props b/src/CodingWithCalvin.VsixSdk/Sdk/Sdk.Vsix.Templates.props
index 7f672e6..efac91d 100644
--- a/src/CodingWithCalvin.VsixSdk/Sdk/Sdk.Vsix.Templates.props
+++ b/src/CodingWithCalvin.VsixSdk/Sdk/Sdk.Vsix.Templates.props
@@ -18,6 +18,9 @@
true
+
+ true
+
ProjectTemplates
ItemTemplates
diff --git a/src/CodingWithCalvin.VsixSdk/Sdk/Sdk.Vsix.Templates.targets b/src/CodingWithCalvin.VsixSdk/Sdk/Sdk.Vsix.Templates.targets
index d6fa714..bfaf9a9 100644
--- a/src/CodingWithCalvin.VsixSdk/Sdk/Sdk.Vsix.Templates.targets
+++ b/src/CodingWithCalvin.VsixSdk/Sdk/Sdk.Vsix.Templates.targets
@@ -5,13 +5,21 @@
Template support for VSIX projects. Handles:
- Auto-discovery of template folders for validation
- Copying templates from referenced projects (VsixTemplateReference)
+ - Auto-injection of Content entries into the manifest
- Validation warnings for manifest configuration
Note: VSSDK handles template packaging when the manifest contains
entries. This file provides
- discovery, cross-project template support, and validation.
+ discovery, cross-project template support, auto-injection, and validation.
-->
+
+
+
+
+
+
+
+ <_IntermediateVsixManifestPath>$(IntermediateOutputPath)source.extension.vsixmanifest
+
+
+
+
+ <_HasProjectTemplates Condition="'@(VsixProjectTemplate)' != ''">true
+ <_HasProjectTemplates Condition="'@(VsixTemplateReference->WithMetadataValue('TemplateType', 'Project'))' != ''">true
+ <_HasItemTemplates Condition="'@(VsixItemTemplate)' != ''">true
+ <_HasItemTemplates Condition="'@(VsixTemplateReference->WithMetadataValue('TemplateType', 'Item'))' != ''">true
+
+
+
+
+
+
+
+
+
+
+ $(_IntermediateVsixManifestPath)
+
+
+
+
+
+
+ Designer
+
+
+
+
+ Condition="'$(AutoInjectVsixTemplateContent)' != 'true' and ('@(VsixProjectTemplate)' != '' or '@(VsixItemTemplate)' != '' or '@(VsixTemplateReference)' != '') and '$(_SourceVsixManifestPath)' != ''">
diff --git a/tests/e2e/Directory.Build.targets b/tests/e2e/Directory.Build.targets
index f327c48..19f606b 100644
--- a/tests/e2e/Directory.Build.targets
+++ b/tests/e2e/Directory.Build.targets
@@ -4,6 +4,11 @@
These add VSIX build behavior without importing Microsoft.NET.Sdk again.
-->
+
+
+
diff --git a/tests/e2e/E2E.Templates.AutoInject/E2E.Templates.AutoInject.csproj b/tests/e2e/E2E.Templates.AutoInject/E2E.Templates.AutoInject.csproj
new file mode 100644
index 0000000..3da6fa0
--- /dev/null
+++ b/tests/e2e/E2E.Templates.AutoInject/E2E.Templates.AutoInject.csproj
@@ -0,0 +1,20 @@
+
+
+
+
+ 1.0.0
+
+
+
+
+
+
+
diff --git a/tests/e2e/E2E.Templates.AutoInject/E2ETemplatesAutoInjectPackage.cs b/tests/e2e/E2E.Templates.AutoInject/E2ETemplatesAutoInjectPackage.cs
new file mode 100644
index 0000000..3db99d9
--- /dev/null
+++ b/tests/e2e/E2E.Templates.AutoInject/E2ETemplatesAutoInjectPackage.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Threading;
+using Microsoft.VisualStudio.Shell;
+using Task = System.Threading.Tasks.Task;
+
+namespace E2E.Templates.AutoInject
+{
+ [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
+ [Guid("00000000-0000-0000-0000-000000000010")]
+ public sealed class E2ETemplatesAutoInjectPackage : AsyncPackage
+ {
+ protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress)
+ {
+ await base.InitializeAsync(cancellationToken, progress);
+ await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+ }
+ }
+}
diff --git a/tests/e2e/E2E.Templates.AutoInject/ProjectTemplates/TestProject/Program.cs b/tests/e2e/E2E.Templates.AutoInject/ProjectTemplates/TestProject/Program.cs
new file mode 100644
index 0000000..503c1cc
--- /dev/null
+++ b/tests/e2e/E2E.Templates.AutoInject/ProjectTemplates/TestProject/Program.cs
@@ -0,0 +1,9 @@
+namespace $safeprojectname$;
+
+class Program
+{
+ static void Main(string[] args)
+ {
+ Console.WriteLine("Hello, World!");
+ }
+}
diff --git a/tests/e2e/E2E.Templates.AutoInject/ProjectTemplates/TestProject/Project.csproj b/tests/e2e/E2E.Templates.AutoInject/ProjectTemplates/TestProject/Project.csproj
new file mode 100644
index 0000000..d65ced5
--- /dev/null
+++ b/tests/e2e/E2E.Templates.AutoInject/ProjectTemplates/TestProject/Project.csproj
@@ -0,0 +1,9 @@
+
+
+
+ Exe
+ net8.0
+ $safeprojectname$
+
+
+
diff --git a/tests/e2e/E2E.Templates.AutoInject/ProjectTemplates/TestProject/TestProject.vstemplate b/tests/e2e/E2E.Templates.AutoInject/ProjectTemplates/TestProject/TestProject.vstemplate
new file mode 100644
index 0000000..cb7c261
--- /dev/null
+++ b/tests/e2e/E2E.Templates.AutoInject/ProjectTemplates/TestProject/TestProject.vstemplate
@@ -0,0 +1,17 @@
+
+
+
+ E2E Test Project
+ A test project template for auto-inject E2E testing
+ CSharp
+ 1000
+ TestProject
+ true
+ true
+
+
+
+ Program.cs
+
+
+
diff --git a/tests/e2e/E2E.Templates.AutoInject/source.extension.vsixmanifest b/tests/e2e/E2E.Templates.AutoInject/source.extension.vsixmanifest
new file mode 100644
index 0000000..10adf41
--- /dev/null
+++ b/tests/e2e/E2E.Templates.AutoInject/source.extension.vsixmanifest
@@ -0,0 +1,20 @@
+
+
+
+
+ E2E Templates AutoInject Test
+ E2E test for automatic Content injection into manifest
+
+
+
+
+
+
+
+
+
+
+
+