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 + + + + amd64 + + + + + + + + + +