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
13 changes: 13 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down Expand Up @@ -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

Expand Down
50 changes: 35 additions & 15 deletions docs/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<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.
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.

## Item Types

Expand Down Expand Up @@ -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 `<Content>` 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 `<Content>` entries into the manifest automatically

### Disabling Auto-Discovery

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

## Manifest Configuration

Visual Studio requires `<Content>` entries in your `.vsixmanifest` to register templates. Add these to your manifest:
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.

### Automatic Content Injection (Default)

When `AutoInjectVsixTemplateContent` is enabled (the default), the SDK:
1. Creates an intermediate manifest in the `obj` folder
2. Injects `<ProjectTemplate Path="ProjectTemplates"/>` if project templates are discovered
3. Injects `<ItemTemplate Path="ItemTemplates"/>` 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 `<Content>` section at all when using templates.

### Disabling Auto-Injection

If you prefer to manage the manifest Content entries manually, disable auto-injection:

```xml
<PropertyGroup>
<AutoInjectVsixTemplateContent>false</AutoInjectVsixTemplateContent>
</PropertyGroup>
```

When auto-injection is disabled, you must add the Content entries to your manifest manually:

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

These warnings only appear when `AutoInjectVsixTemplateContent` is set to `false`.

## Complete Example

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

### Manifest File

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

```xml
<?xml version="1.0" encoding="utf-8"?>
Expand All @@ -157,10 +179,7 @@ Add the `<Content>` entries for your templates:
<ProductArchitecture>amd64</ProductArchitecture>
</InstallationTarget>
</Installation>
<Content>
<ProjectTemplate Path="ProjectTemplates" />
<ItemTemplate Path="ItemTemplates" />
</Content>
<!-- Content entries are auto-injected by the SDK -->
</PackageManifest>
```

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

### Templates not appearing in Visual Studio

1. Ensure your manifest has the appropriate `<Content>` 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 `<Content>` entries
3. Verify the `.vstemplate` file has correct `<ProjectType>` or `<TemplateGroupID>`
4. Reset the Visual Studio template cache: delete `%LocalAppData%\Microsoft\VisualStudio\<version>\ComponentModelCache`
5. If using `AutoInjectVsixTemplateContent=false`, ensure your source manifest has the `<Content>` entries

### Build errors about missing template folders

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Build.Framework" Version="17.3.2" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.3.2" ExcludeAssets="runtime" />
</ItemGroup>

</Project>
156 changes: 156 additions & 0 deletions src/CodingWithCalvin.VsixSdk.Tasks/InjectVsixManifestContentTask.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
using System;
using System.IO;
using System.Xml;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace CodingWithCalvin.VsixSdk.Tasks;

/// <summary>
/// MSBuild task that injects Content entries into a VSIX manifest for discovered templates.
/// Creates an intermediate manifest file without modifying the source.
/// </summary>
public class InjectVsixManifestContentTask : Task
{
private const string VsixNamespace = "http://schemas.microsoft.com/developer/vsx-schema/2011";

/// <summary>
/// Path to the source VSIX manifest file.
/// </summary>
[Required]
public string SourceManifestPath { get; set; } = string.Empty;

/// <summary>
/// Path where the modified manifest will be written.
/// </summary>
[Required]
public string OutputManifestPath { get; set; } = string.Empty;

/// <summary>
/// Whether project templates were discovered.
/// </summary>
public bool HasProjectTemplates { get; set; }

/// <summary>
/// Whether item templates were discovered.
/// </summary>
public bool HasItemTemplates { get; set; }

/// <summary>
/// The folder path for project templates (default: "ProjectTemplates").
/// </summary>
public string ProjectTemplatesPath { get; set; } = "ProjectTemplates";

/// <summary>
/// The folder path for item templates (default: "ItemTemplates").
/// </summary>
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;
}
}
}
15 changes: 15 additions & 0 deletions src/CodingWithCalvin.VsixSdk/CodingWithCalvin.VsixSdk.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@
</ProjectReference>
</ItemGroup>

<!-- Reference the tasks project -->
<ItemGroup>
<ProjectReference Include="..\CodingWithCalvin.VsixSdk.Tasks\CodingWithCalvin.VsixSdk.Tasks.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
<PrivateAssets>all</PrivateAssets>
</ProjectReference>
</ItemGroup>

<!-- Include the SDK files in the package at the correct location -->
<ItemGroup>
<!-- The Sdk folder must be at the root of the package -->
Expand All @@ -54,6 +63,12 @@
PackagePath="analyzers\dotnet\cs"
Visible="false" />

<!-- Include the MSBuild tasks -->
<None Include="..\CodingWithCalvin.VsixSdk.Tasks\bin\$(Configuration)\net472\CodingWithCalvin.VsixSdk.Tasks.dll"
Pack="true"
PackagePath="build"
Visible="false" />

<!-- Include README at package root -->
<None Include="..\..\README.md" Pack="true" PackagePath="" />
</ItemGroup>
Expand Down
3 changes: 3 additions & 0 deletions src/CodingWithCalvin.VsixSdk/Sdk/Sdk.Vsix.Templates.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
<!-- Enable/disable automatic template discovery -->
<EnableDefaultVsixTemplateItems Condition="'$(EnableDefaultVsixTemplateItems)' == ''">true</EnableDefaultVsixTemplateItems>

<!-- Enable/disable automatic Content entry injection for discovered templates -->
<AutoInjectVsixTemplateContent Condition="'$(AutoInjectVsixTemplateContent)' == ''">true</AutoInjectVsixTemplateContent>

<!-- Default folders for template discovery -->
<VsixProjectTemplatesFolder Condition="'$(VsixProjectTemplatesFolder)' == ''">ProjectTemplates</VsixProjectTemplatesFolder>
<VsixItemTemplatesFolder Condition="'$(VsixItemTemplatesFolder)' == ''">ItemTemplates</VsixItemTemplatesFolder>
Expand Down
Loading