From 8ba482f76bbc08014c28d17836e195d86234d050 Mon Sep 17 00:00:00 2001 From: "Calvin A. Allen" Date: Tue, 30 Dec 2025 14:56:29 -0500 Subject: [PATCH 1/3] feat(sdk): add publish manifest generation and source generators - Add publish.manifest.json generation from VSIX manifest values - publisher from Identity/@Publisher - categories from Tags element - repo from MoreInfo element - Add source generators for VsixInfo and VSCT constants - Add README template to dotnet new template - Add Bluesky and LinkedIn notifications to release workflow - Add contributors workflow - Update README with publish manifest documentation --- .github/workflows/contributors.yml | 40 ++ .github/workflows/release.yml | 42 ++ .gitignore | 5 + README.md | 372 ++++++++++++++---- samples/Directory.Build.targets | 5 + samples/SampleExtension/SampleCommands.vsct | 32 ++ samples/SampleExtension/publish.manifest.json | 13 + .../source.extension.vsixmanifest | 9 +- ...CodingWithCalvin.VsixSdk.Generators.csproj | 35 ++ .../VsctGuidsGenerator.cs | 205 ++++++++++ .../VsixInfoGenerator.cs | 195 +++++++++ .../.template.config/template.json | 39 +- .../templates/vsix-extension/README.md | 31 ++ .../vsix-extension/VsixExtension.csproj | 4 + .../vsix-extension/VsixExtension.slnx | 3 + .../source.extension.vsixmanifest | 6 +- src/CodingWithCalvin.VsixSdk.slnx | 1 + .../CodingWithCalvin.VsixSdk.csproj | 15 + .../Sdk/Sdk.Vsix.props | 41 +- .../Sdk/Sdk.Vsix.targets | 165 +++++++- 20 files changed, 1145 insertions(+), 113 deletions(-) create mode 100644 .github/workflows/contributors.yml create mode 100644 samples/SampleExtension/SampleCommands.vsct create mode 100644 samples/SampleExtension/publish.manifest.json create mode 100644 src/CodingWithCalvin.VsixSdk.Generators/CodingWithCalvin.VsixSdk.Generators.csproj create mode 100644 src/CodingWithCalvin.VsixSdk.Generators/VsctGuidsGenerator.cs create mode 100644 src/CodingWithCalvin.VsixSdk.Generators/VsixInfoGenerator.cs create mode 100644 src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/README.md create mode 100644 src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/VsixExtension.slnx diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml new file mode 100644 index 0000000..78918a7 --- /dev/null +++ b/.github/workflows/contributors.yml @@ -0,0 +1,40 @@ +name: Update Contributors + +on: + schedule: + - cron: '0 0 * * *' # Run daily at midnight UTC + workflow_dispatch: # Allow manual trigger + +jobs: + contributors: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.CONTRIBUTORS_TOKEN }} + + - name: Update contributors + env: + GH_TOKEN: ${{ secrets.CONTRIBUTORS_TOKEN }} + run: | + # Fetch contributors from GitHub API (exclude bots) - markdown format + contributors=$(gh api repos/CodingWithCalvin/VsixSdk/contributors --paginate --jq '.[] | select(.type != "Bot") | select(.login | test("\\[bot\\]$") | not) | "[![\(.login)](\(.avatar_url)&s=64)](\(.html_url))"' | tr '\n' ' ') + + # Build the contributors section + contrib_section=" +$contributors +" + + # Update README between the markers + awk -v contrib="$contrib_section" ' + //{found=1; print contrib; next} + //{found=0; next} + !found{print} + ' README.md > README.tmp && mv README.tmp README.md + + - name: Commit and push + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add README.md + git diff --staged --quiet || (git commit -m "docs: update contributors [skip ci]" && git push) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5836309..9fcc5ff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,8 @@ jobs: release: needs: changelog runs-on: windows-latest + outputs: + version: ${{ steps.version.outputs.VERSION }} permissions: contents: write @@ -64,3 +66,43 @@ jobs: files: | artifacts/packages/CodingWithCalvin.VsixSdk.${{ steps.version.outputs.VERSION }}.nupkg artifacts/packages/CodingWithCalvin.VsixSdk.Templates.${{ steps.version.outputs.VERSION }}.nupkg + + notify-bluesky: + needs: release + uses: CodingWithCalvin/.github/.github/workflows/bluesky-post.yml@main + with: + post_text: | + 🚀 CodingWithCalvin.VsixSdk v${{ needs.release.outputs.version }} has been released! + + Build modern SDK-style Visual Studio extensions with ease. + + [📋 Release Notes](https://github.com/${{ github.repository }}/releases/tag/v${{ needs.release.outputs.version }}) + [📦 NuGet](https://www.nuget.org/packages/CodingWithCalvin.VsixSdk) + + #dotnet #csharp #visualstudio #nuget + embed_url: https://www.nuget.org/packages/CodingWithCalvin.VsixSdk + embed_title: CodingWithCalvin.VsixSdk + embed_description: An MSBuild SDK for modern SDK-style Visual Studio extension development + secrets: + BLUESKY_USERNAME: ${{ secrets.BLUESKY_USERNAME }} + BLUESKY_APP_PASSWORD: ${{ secrets.BLUESKY_APP_PASSWORD }} + + notify-linkedin: + needs: release + uses: CodingWithCalvin/.github/.github/workflows/linkedin-post.yml@main + with: + post_text: | + 🚀 CodingWithCalvin.VsixSdk v${{ needs.release.outputs.version }} has been released! + + Build modern SDK-style Visual Studio extensions with ease. + + 📋 Release Notes: https://github.com/${{ github.repository }}/releases/tag/v${{ needs.release.outputs.version }} + 📦 NuGet: https://www.nuget.org/packages/CodingWithCalvin.VsixSdk + + #dotnet #csharp #visualstudio #nuget + article_url: https://www.nuget.org/packages/CodingWithCalvin.VsixSdk + article_title: CodingWithCalvin.VsixSdk + article_description: An MSBuild SDK for modern SDK-style Visual Studio extension development + secrets: + LINKEDIN_ACCESS_TOKEN: ${{ secrets.LINKEDIN_ACCESS_TOKEN }} + LINKEDIN_CLIENT_ID: ${{ secrets.LINKEDIN_CLIENT_ID }} diff --git a/.gitignore b/.gitignore index 90fec5f..469802b 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,8 @@ Desktop.ini CLAUDE.local.md .claude/ + +# Generated files from SDK +Generated/ +VsixInfo.g.cs +*Vsct.g.cs diff --git a/README.md b/README.md index 2652373..52bf847 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,71 @@ -# 🚀 CodingWithCalvin.VsixSdk +# CodingWithCalvin.VsixSdk [![NuGet](https://img.shields.io/nuget/v/CodingWithCalvin.VsixSdk.svg)](https://www.nuget.org/packages/CodingWithCalvin.VsixSdk) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -> ✨ **Finally!** Modern SDK-style projects for Visual Studio extensions! +An MSBuild SDK that brings modern SDK-style `.csproj` files to Visual Studio extension development. No more XML soup! -An MSBuild SDK that brings the clean, modern `.csproj` format to VSIX development. No more XML soup! 🎉 +## Why This Exists ---- +Visual Studio extension projects are stuck in the past. They still use the old, verbose project format while the rest of .NET has moved on to clean SDK-style projects. -## 🤔 Why This Exists +**This SDK fixes that.** -Visual Studio extension projects are stuck in 2010. They still use the old, verbose project format while the rest of .NET has moved on to beautiful SDK-style projects. +Write clean `.csproj` files. Get source generators for compile-time constants. Ship fully functional VSIX packages. -**This SDK fixes that.** +## Getting Started -Write clean `.csproj` files. Get all the modern tooling. Ship fully functional VSIX packages. 💪 +### Using the Template (Recommended) ---- +The easiest way to create a new VSIX project is with the dotnet template: -## 📦 Installation +```bash +# Install the template +dotnet new install CodingWithCalvin.VsixSdk.Templates -``` -dotnet add package CodingWithCalvin.VsixSdk +# Create a new extension +dotnet new vsix -n MyExtension --publisher "Your Name" --description "My awesome extension" + +# Build it +cd MyExtension +dotnet build ``` -Or reference it directly in your project file (recommended): +#### Template Parameters -```xml - -``` +| Parameter | Short | Description | Default | +|-----------|-------|-------------|---------| +| `--extensionName` | `-e` | Display name in VS extension manager | Project name | +| `--publisher` | `-p` | Publisher name in VSIX manifest | MyPublisher | +| `--description` | `-de` | Extension description | A Visual Studio extension | +| `--tags` | `-ta` | Comma-separated tags for discoverability | extension | ---- +**Examples:** + +```bash +# Basic - uses project name as display name +dotnet new vsix -n MyExtension + +# With custom extension name (different from project name) +dotnet new vsix -n MyExtension.Vsix --extensionName "My Cool Extension" + +# With all parameters +dotnet new vsix -n MyExtension \ + --extensionName "My Cool Extension" \ + --publisher "Acme Corp" \ + --description "Adds productivity features to Visual Studio" \ + --tags "productivity, tools, editor" +``` -## ⚡ Quick Start +### Manual Setup -### 1️⃣ Create the Project File +If you prefer to set up manually, create a `.csproj` file: ```xml net472 - 1.0.0 @@ -53,17 +76,13 @@ Or reference it directly in your project file (recommended): ``` -That's it. Seriously. 😎 - -### 2️⃣ Create the VSIX Manifest - -Create `source.extension.vsixmanifest`: +Then create `source.extension.vsixmanifest`: ```xml - + My Extension Description of your extension your, tags, here @@ -85,81 +104,158 @@ Create `source.extension.vsixmanifest`: ``` -> 💡 **Pro tip:** The `Version="|%CurrentProject%;GetVsixVersion|"` syntax automatically syncs with your project's `Version` property! +## Features + +### Source Generators + +The SDK includes source generators that create compile-time constants from your manifest files. -### 3️⃣ Create Your Package Class +#### VsixInfo - VSIX Manifest Constants + +A `VsixInfo` class is automatically generated from your `source.extension.vsixmanifest`: ```csharp -using System; -using System.Runtime.InteropServices; -using System.Threading; -using Microsoft.VisualStudio.Shell; -using Task = System.Threading.Tasks.Task; +// Auto-generated from your manifest +public static class VsixInfo +{ + public const string Id = "MyExtension.a1b2c3d4-..."; + public const string Version = "1.0.0"; + public const string Publisher = "Your Name"; + public const string DisplayName = "My Extension"; + public const string Description = "Description of your extension"; + public const string Tags = "your, tags, here"; + // ... and more +} +``` + +Use it in your code: + +```csharp +// Display version in your extension +MessageBox.Show($"Version: {VsixInfo.Version}"); + +// Use in attributes +[Guid(VsixInfo.Id)] +public sealed class MyPackage : AsyncPackage { } +``` -namespace MyExtension +#### VSCT GUIDs and IDs + +If you have `.vsct` files, constants are generated for your GUIDs and command IDs: + +```csharp +// Auto-generated from MyCommands.vsct +public static class MyCommandsVsct { - [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] - [Guid("YOUR-GUID-HERE")] - public sealed class MyExtensionPackage : AsyncPackage - { - protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) - { - await base.InitializeAsync(cancellationToken, progress); - await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + public static readonly Guid guidMyPackage = new Guid("..."); - // Your initialization code here 🎨 - } + public static class guidMyCommandSet + { + public const string GuidString = "..."; + public static readonly Guid Guid = new Guid(GuidString); + public const int MyCommandId = 0x0100; + public const int MyMenuGroup = 0x1020; } } ``` -### 4️⃣ Build and Debug +#### Generated Files Location -| Action | Command | -|--------|---------| -| 🔨 Build | `dotnet build` or build in Visual Studio | -| 🐛 Debug | Press **F5** → launches the Experimental Instance | +Generated source files are written to the `Generated/` folder in your project and are visible in Solution Explorer. They're marked as auto-generated so you know not to edit them directly. ---- +### Version Override + +Update the VSIX version at build time without manually editing the manifest: -## ✅ Features +```bash +dotnet build -p:SetVsixVersion=2.0.0 +``` -| Feature | Description | -|---------|-------------| -| 📝 **SDK-style projects** | Clean, minimal `.csproj` files | -| 🐛 **F5 debugging** | Works out of the box with VS Experimental Instance | -| 📁 **Auto-inclusion** | VSCT, VSIX manifests, and VSPackage resources included automatically | -| 🔄 **Version sync** | VSIX version derived from project `Version` property | -| ⚙️ **Sensible defaults** | Correct settings for VS 2022+ (x64, .NET Framework 4.7.2+) | -| 🚀 **Smart deployment** | Only deploys to Experimental Instance when building in VS | +This updates the `source.extension.vsixmanifest` file with the new version, rebuilds with the correct version in all outputs (including the generated `VsixInfo.Version` constant), and produces the VSIX with the specified version. ---- +This is useful for CI/CD pipelines: -## 📋 Requirements +```yaml +# GitHub Actions example +- name: Build Release + run: dotnet build -c Release -p:SetVsixVersion=${{ github.ref_name }} +``` -- 🖥️ Visual Studio 2022 or later -- 🎯 .NET Framework 4.7.2+ target framework +### Auto-Inclusion ---- +The SDK automatically includes common VSIX files: + +- `*.vsct` files as `VSCTCompile` items +- `VSPackage.resx` files with proper metadata +- `source.extension.vsixmanifest` as an `AdditionalFile` for source generators + +### F5 Debugging + +Press F5 to launch the Visual Studio Experimental Instance with your extension loaded. This works automatically when: +- Building in Debug configuration +- Building inside Visual Studio (not `dotnet build`) + +### Smart Deployment + +Extensions are only deployed to the Experimental Instance when building inside Visual Studio. This prevents errors when building from the command line. + +### Publish Manifest Generation + +The SDK automatically generates a `publish.manifest.json` file for publishing to the VS Marketplace. All values are extracted from your VSIX manifest: + +```json +{ + "$schema": "http://json.schemastore.org/vsix-publish", + "categories": [ + "your", "tags", "here" + ], + "identity": { + "internalName": "MyExtension" + }, + "overview": "README.md", + "publisher": "Your Name", + "qna": true, + "repo": "https://github.com/you/your-repo" +} +``` + +| JSON Field | Source | +|------------|--------| +| `publisher` | `Identity/@Publisher` in VSIX manifest | +| `categories` | `Tags` element in VSIX manifest | +| `repo` | `MoreInfo` element in VSIX manifest | +| `internalName` | Project name | +| `overview` | Configurable via `VsixPublishOverview` property (default: `README.md`) | +| `qna` | Configurable via `VsixPublishQnA` property (default: `true`) | + +To disable publish manifest generation: + +```xml + + false + +``` -## 🔧 Configuration +## Configuration ### Properties | Property | Default | Description | |----------|---------|-------------| | `TargetFramework` | `net472` | Target framework (must be .NET Framework 4.7.2+) | -| `Platform` | `x64` | Target platform (VS 2022+ is 64-bit) | -| `UseCodebase` | `true` | Use codebase for assembly loading | +| `Platform` | `AnyCPU` | Target platform | | `GeneratePkgDefFile` | `true` | Generate .pkgdef registration file | -| `VsixVersion` | `$(Version)` | VSIX manifest version | | `DeployExtension` | `true`* | Deploy to experimental instance | | `EnableDefaultVsixItems` | `true` | Auto-include VSIX-related files | -| `EnableDefaultVsixDebugging` | `true` | Configure F5 debugging | +| `EmitCompilerGeneratedFiles` | `true` | Write generated source files to disk | +| `CompilerGeneratedFilesOutputPath` | `Generated/` | Location for generated source files | +| `GeneratePublishManifest` | `true` | Generate `publish.manifest.json` for VS Marketplace | +| `VsixPublishOverview` | `README.md` | Path to README for marketplace overview | +| `VsixPublishQnA` | `true` | Enable Q&A on marketplace listing | > \* Only when `Configuration=Debug` AND building inside Visual Studio -### 🎛️ Disabling Auto-Inclusion +### Disabling Auto-Inclusion Take full control over which files are included: @@ -174,12 +270,11 @@ Or disable specific categories: ```xml false - false false ``` -### 🐛 Custom Debugging Arguments +### Custom Debugging Arguments ```xml @@ -187,21 +282,122 @@ Or disable specific categories: ``` ---- +## Migrating from Legacy Projects -## 🔄 Migration from Legacy Projects +If you have an existing non-SDK style VSIX project, follow these steps to convert it. -Migrating from the old project format? Here's how: +### Step 1: Back Up Your Project -1. 📝 Replace your old `.csproj` content with the SDK-style format above -2. 🗑️ Remove unnecessary `` statements — the SDK handles them -3. 📁 Keep your `source.extension.vsixmanifest`, `.vsct`, and resource files -4. ➕ Add `amd64` to your manifest for VS 2022+ -5. 🔨 Build and test! +Before making changes, ensure your project is committed to source control or backed up. ---- +### Step 2: Replace the .csproj Content + +Replace your entire `.csproj` file with the SDK-style format: + +**Before (Legacy):** +```xml + + + + + Debug + AnyCPU + {YOUR-GUID} + Library + Properties + MyExtension + MyExtension + v4.7.2 + + + + + + +``` + +**After (SDK-style):** +```xml + + + + net472 + MyExtension + MyExtension + + + + + + + + +``` + +### Step 3: Update the VSIX Manifest for VS 2022+ + +Add `amd64` to each `InstallationTarget`: + +```xml + + + amd64 + + + +``` + +### Step 4: Remove Unnecessary Files + +Delete these files if they exist (the SDK handles them automatically): +- `packages.config` - Use `PackageReference` instead +- `Properties/AssemblyInfo.cs` - SDK generates this automatically +- `app.config` - Usually not needed + +### Step 5: Update Package References + +Convert from `packages.config` to `PackageReference` format in your `.csproj`: + +```xml + + + + + +``` + +### Step 6: Handle VSCT Files -## 🏗️ Building from Source +If you have `.vsct` files, they're automatically included. Remove any explicit `` items unless you need custom metadata. + +### Step 7: Build and Test + +```bash +dotnet build +``` + +Fix any errors that arise. Common issues: +- **Missing types**: Add the appropriate `PackageReference` +- **Duplicate files**: Remove explicit includes that conflict with auto-inclusion +- **Resource files**: Ensure `VSPackage.resx` files are in the project + +### Migration Checklist + +- [ ] Replaced `.csproj` with SDK-style format +- [ ] Added `amd64` to manifest +- [ ] Converted `packages.config` to `PackageReference` +- [ ] Removed `Properties/AssemblyInfo.cs` +- [ ] Removed explicit file includes (now auto-included) +- [ ] Updated version range to `[17.0, 19.0)` for VS 2022+ +- [ ] Build succeeds with `dotnet build` +- [ ] F5 debugging works in Visual Studio + +## Requirements + +- Visual Studio 2022 or later +- .NET Framework 4.7.2+ target framework + +## Building from Source ```bash # Clone the repository @@ -211,12 +407,18 @@ cd VsixSdk # Build the SDK package dotnet build src/CodingWithCalvin.VsixSdk/CodingWithCalvin.VsixSdk.csproj -c Release -# 📦 Package outputs to artifacts/packages/ +# Build the template package +dotnet pack src/CodingWithCalvin.VsixSdk.Templates/CodingWithCalvin.VsixSdk.Templates.csproj -c Release + +# Packages output to artifacts/packages/ ``` ---- +## Contributors + + + -## 📄 License +## License MIT License - see [LICENSE](LICENSE) for details. diff --git a/samples/Directory.Build.targets b/samples/Directory.Build.targets index 487fe61..d30ea9c 100644 --- a/samples/Directory.Build.targets +++ b/samples/Directory.Build.targets @@ -7,4 +7,9 @@ + + + + + diff --git a/samples/SampleExtension/SampleCommands.vsct b/samples/SampleExtension/SampleCommands.vsct new file mode 100644 index 0000000..0c62cff --- /dev/null +++ b/samples/SampleExtension/SampleCommands.vsct @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/SampleExtension/publish.manifest.json b/samples/SampleExtension/publish.manifest.json new file mode 100644 index 0000000..fc4ef1a --- /dev/null +++ b/samples/SampleExtension/publish.manifest.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json.schemastore.org/vsix-publish", + "categories": [ + "sample", "test" + ], + "identity": { + "internalName": "SampleExtension" + }, + "overview": "README.md", + "publisher": "Coding With Calvin", + "qna": true, + "repo": "https://github.com/CodingWithCalvin/VsixSdk" +} diff --git a/samples/SampleExtension/source.extension.vsixmanifest b/samples/SampleExtension/source.extension.vsixmanifest index 88d23b8..fcfba1a 100644 --- a/samples/SampleExtension/source.extension.vsixmanifest +++ b/samples/SampleExtension/source.extension.vsixmanifest @@ -1,9 +1,10 @@ - + - + Sample Extension - A sample Visual Studio extension built with CodingWithCalvin.VsixSdk + A sample Visual Studio extension built with CodingWithCalvin.VsixSdk + https://github.com/CodingWithCalvin/VsixSdk sample, test @@ -26,4 +27,4 @@ - + \ No newline at end of file diff --git a/src/CodingWithCalvin.VsixSdk.Generators/CodingWithCalvin.VsixSdk.Generators.csproj b/src/CodingWithCalvin.VsixSdk.Generators/CodingWithCalvin.VsixSdk.Generators.csproj new file mode 100644 index 0000000..0dd9428 --- /dev/null +++ b/src/CodingWithCalvin.VsixSdk.Generators/CodingWithCalvin.VsixSdk.Generators.csproj @@ -0,0 +1,35 @@ + + + + netstandard2.0 + latest + enable + true + + + true + false + true + true + + + CodingWithCalvin.VsixSdk.Generators + Source generators for CodingWithCalvin.VsixSdk - generates code from VSIX manifest and VSCT files + Calvin A. Allen + vsix;visualstudio;extension;sdk;sourcegenerator;roslyn + MIT + https://github.com/CodingWithCalvin/VsixSdk + git + + + + + + + + + + + + + diff --git a/src/CodingWithCalvin.VsixSdk.Generators/VsctGuidsGenerator.cs b/src/CodingWithCalvin.VsixSdk.Generators/VsctGuidsGenerator.cs new file mode 100644 index 0000000..5f731a1 --- /dev/null +++ b/src/CodingWithCalvin.VsixSdk.Generators/VsctGuidsGenerator.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace CodingWithCalvin.VsixSdk.Generators; + +/// +/// Source generator that creates static classes with GUIDs and IDs from VSCT files. +/// +[Generator] +public class VsctGuidsGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Find all .vsct files in AdditionalFiles + var vsctFiles = context.AdditionalTextsProvider + .Where(file => file.Path.EndsWith(".vsct", StringComparison.OrdinalIgnoreCase)); + + // Combine with compilation to get namespace + var compilationAndVscts = context.CompilationProvider + .Combine(vsctFiles.Collect()); + + context.RegisterSourceOutput(compilationAndVscts, (ctx, source) => + { + var (compilation, vscts) = source; + + foreach (var vsct in vscts) + { + GenerateVsctGuids(ctx, compilation, vsct); + } + }); + } + + private static void GenerateVsctGuids( + SourceProductionContext context, + Compilation compilation, + AdditionalText vsctFile) + { + var text = vsctFile.GetText(context.CancellationToken); + if (text == null) return; + + var fileName = Path.GetFileNameWithoutExtension(vsctFile.Path); + var className = $"{fileName}Vsct"; + + try + { + var doc = new XmlDocument(); + doc.LoadXml(text.ToString()); + + var nsmgr = new XmlNamespaceManager(doc.NameTable); + nsmgr.AddNamespace("vsct", "http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable"); + + // Extract all GuidSymbols + var guidSymbols = ExtractGuidSymbols(doc, nsmgr); + + if (guidSymbols.Count == 0) return; + + // Get namespace from compilation + var rootNamespace = compilation.AssemblyName ?? "GeneratedCode"; + + var source = GenerateSource(rootNamespace, className, fileName, guidSymbols); + context.AddSource($"{className}.g.cs", SourceText.From(source, Encoding.UTF8)); + } + catch (Exception ex) + { + // Report diagnostic on error + var descriptor = new DiagnosticDescriptor( + "VSIXSDK002", + "Failed to parse VSCT file", + "Failed to parse VSCT file '{0}': {1}", + "VsixSdk", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + context.ReportDiagnostic(Diagnostic.Create(descriptor, Location.None, vsctFile.Path, ex.Message)); + } + } + + private static List ExtractGuidSymbols(XmlDocument doc, XmlNamespaceManager nsmgr) + { + var result = new List(); + + // Try both namespaced and non-namespaced queries + var guidSymbolNodes = doc.SelectNodes("//vsct:GuidSymbol", nsmgr); + if (guidSymbolNodes == null || guidSymbolNodes.Count == 0) + { + guidSymbolNodes = doc.SelectNodes("//GuidSymbol"); + } + + if (guidSymbolNodes == null) return result; + + foreach (XmlNode node in guidSymbolNodes) + { + var name = node.Attributes?["name"]?.Value; + var value = node.Attributes?["value"]?.Value; + + if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(value)) + continue; + + var guidSymbol = new GuidSymbol + { + Name = name!, + Value = value!.Trim('{', '}') + }; + + // Extract nested IDSymbols + var idSymbolNodes = node.SelectNodes("vsct:IDSymbol", nsmgr); + if (idSymbolNodes == null || idSymbolNodes.Count == 0) + { + idSymbolNodes = node.SelectNodes("IDSymbol"); + } + + if (idSymbolNodes != null) + { + foreach (XmlNode idNode in idSymbolNodes) + { + var idName = idNode.Attributes?["name"]?.Value; + var idValue = idNode.Attributes?["value"]?.Value; + + if (!string.IsNullOrEmpty(idName) && !string.IsNullOrEmpty(idValue)) + { + guidSymbol.IdSymbols.Add(new IdSymbol { Name = idName!, Value = idValue! }); + } + } + } + + result.Add(guidSymbol); + } + + return result; + } + + private static string GenerateSource(string rootNamespace, string className, string fileName, List guidSymbols) + { + var sb = new StringBuilder(); + sb.AppendLine("//------------------------------------------------------------------------------"); + sb.AppendLine("// "); + sb.AppendLine("// This code was generated by CodingWithCalvin.VsixSdk from the VSCT file."); + sb.AppendLine("// Changes to this file may cause incorrect behavior and will be lost if"); + sb.AppendLine("// the code is regenerated."); + sb.AppendLine("// "); + sb.AppendLine("//------------------------------------------------------------------------------"); + sb.AppendLine(); + sb.AppendLine("using System;"); + sb.AppendLine(); + sb.AppendLine($"namespace {rootNamespace}"); + sb.AppendLine("{"); + sb.AppendLine(" /// "); + sb.AppendLine($" /// GUIDs and IDs from {fileName}.vsct"); + sb.AppendLine(" /// "); + sb.AppendLine($" internal static class {className}"); + sb.AppendLine(" {"); + + foreach (var guidSymbol in guidSymbols) + { + if (guidSymbol.IdSymbols.Count == 0) + { + // Simple GUID constant + sb.AppendLine($" /// GUID: {{{guidSymbol.Value}}}"); + sb.AppendLine($" public static readonly Guid {guidSymbol.Name} = new Guid(\"{guidSymbol.Value}\");"); + sb.AppendLine(); + } + else + { + // Nested class with GUID and IDs + sb.AppendLine($" /// Command set GUID: {{{guidSymbol.Value}}}"); + sb.AppendLine($" public static class {guidSymbol.Name}"); + sb.AppendLine(" {"); + sb.AppendLine($" public const string GuidString = \"{guidSymbol.Value}\";"); + sb.AppendLine($" public static readonly Guid Guid = new Guid(GuidString);"); + + foreach (var idSymbol in guidSymbol.IdSymbols) + { + sb.AppendLine($" public const int {idSymbol.Name} = {idSymbol.Value};"); + } + + sb.AppendLine(" }"); + sb.AppendLine(); + } + } + + sb.AppendLine(" }"); + sb.AppendLine("}"); + + return sb.ToString(); + } + + private class GuidSymbol + { + public string Name { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + public List IdSymbols { get; } = new List(); + } + + private class IdSymbol + { + public string Name { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + } +} diff --git a/src/CodingWithCalvin.VsixSdk.Generators/VsixInfoGenerator.cs b/src/CodingWithCalvin.VsixSdk.Generators/VsixInfoGenerator.cs new file mode 100644 index 0000000..9aca016 --- /dev/null +++ b/src/CodingWithCalvin.VsixSdk.Generators/VsixInfoGenerator.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace CodingWithCalvin.VsixSdk.Generators; + +/// +/// Source generator that creates a VsixInfo class from the VSIX manifest file. +/// +[Generator] +public class VsixInfoGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Find all .vsixmanifest files in AdditionalFiles + var manifestFiles = context.AdditionalTextsProvider + .Where(file => file.Path.EndsWith(".vsixmanifest", StringComparison.OrdinalIgnoreCase)); + + // Combine with compilation to get namespace + var compilationAndManifests = context.CompilationProvider + .Combine(manifestFiles.Collect()); + + context.RegisterSourceOutput(compilationAndManifests, (ctx, source) => + { + var (compilation, manifests) = source; + + foreach (var manifest in manifests) + { + GenerateVsixInfo(ctx, compilation, manifest); + } + }); + } + + private static void GenerateVsixInfo( + SourceProductionContext context, + Compilation compilation, + AdditionalText manifestFile) + { + var text = manifestFile.GetText(context.CancellationToken); + if (text == null) return; + + try + { + var doc = new XmlDocument(); + doc.LoadXml(text.ToString()); + + var nsmgr = new XmlNamespaceManager(doc.NameTable); + nsmgr.AddNamespace("vsix", "http://schemas.microsoft.com/developer/vsx-schema/2011"); + + // Extract metadata + var metadata = new VsixMetadata + { + Id = GetAttributeValue(doc, "//vsix:Identity/@Id", nsmgr), + Version = GetAttributeValue(doc, "//vsix:Identity/@Version", nsmgr), + Language = GetAttributeValue(doc, "//vsix:Identity/@Language", nsmgr), + Publisher = GetAttributeValue(doc, "//vsix:Identity/@Publisher", nsmgr), + DisplayName = GetElementText(doc, "//vsix:DisplayName", nsmgr), + Description = GetElementText(doc, "//vsix:Description", nsmgr), + MoreInfo = GetElementText(doc, "//vsix:MoreInfo", nsmgr), + License = GetElementText(doc, "//vsix:License", nsmgr), + GettingStartedGuide = GetElementText(doc, "//vsix:GettingStartedGuide", nsmgr), + ReleaseNotes = GetElementText(doc, "//vsix:ReleaseNotes", nsmgr), + Icon = GetElementText(doc, "//vsix:Icon", nsmgr), + PreviewImage = GetElementText(doc, "//vsix:PreviewImage", nsmgr), + Tags = GetElementText(doc, "//vsix:Tags", nsmgr), + IsPreview = GetElementText(doc, "//vsix:Preview", nsmgr)?.Equals("true", StringComparison.OrdinalIgnoreCase) ?? false + }; + + // Get namespace from compilation + var rootNamespace = compilation.AssemblyName ?? "GeneratedCode"; + + var source = GenerateSource(rootNamespace, metadata); + context.AddSource("VsixInfo.g.cs", SourceText.From(source, Encoding.UTF8)); + } + catch (Exception ex) + { + // Report diagnostic on error + var descriptor = new DiagnosticDescriptor( + "VSIXSDK001", + "Failed to parse VSIX manifest", + "Failed to parse VSIX manifest: {0}", + "VsixSdk", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + context.ReportDiagnostic(Diagnostic.Create(descriptor, Location.None, ex.Message)); + } + } + + private static string? GetAttributeValue(XmlDocument doc, string xpath, XmlNamespaceManager nsmgr) + { + var node = doc.SelectSingleNode(xpath, nsmgr); + return node?.Value; + } + + private static string? GetElementText(XmlDocument doc, string xpath, XmlNamespaceManager nsmgr) + { + var node = doc.SelectSingleNode(xpath, nsmgr); + return node?.InnerText; + } + + private static string GenerateSource(string rootNamespace, VsixMetadata metadata) + { + var sb = new StringBuilder(); + sb.AppendLine("//------------------------------------------------------------------------------"); + sb.AppendLine("// "); + sb.AppendLine("// This code was generated by CodingWithCalvin.VsixSdk from the VSIX manifest."); + sb.AppendLine("// Changes to this file may cause incorrect behavior and will be lost if"); + sb.AppendLine("// the code is regenerated."); + sb.AppendLine("// "); + sb.AppendLine("//------------------------------------------------------------------------------"); + sb.AppendLine(); + sb.AppendLine($"namespace {rootNamespace}"); + sb.AppendLine("{"); + sb.AppendLine(" /// "); + sb.AppendLine(" /// Provides compile-time constants from the VSIX manifest metadata."); + sb.AppendLine(" /// "); + sb.AppendLine(" internal static class VsixInfo"); + sb.AppendLine(" {"); + sb.AppendLine($" /// The unique identifier of the extension."); + sb.AppendLine($" public const string Id = \"{EscapeString(metadata.Id)}\";"); + sb.AppendLine(); + sb.AppendLine($" /// The version of the extension."); + sb.AppendLine($" public const string Version = \"{EscapeString(metadata.Version)}\";"); + sb.AppendLine(); + sb.AppendLine($" /// The language/locale of the extension."); + sb.AppendLine($" public const string Language = \"{EscapeString(metadata.Language)}\";"); + sb.AppendLine(); + sb.AppendLine($" /// The publisher/author of the extension."); + sb.AppendLine($" public const string Publisher = \"{EscapeString(metadata.Publisher)}\";"); + sb.AppendLine(); + sb.AppendLine($" /// The display name of the extension."); + sb.AppendLine($" public const string DisplayName = \"{EscapeString(metadata.DisplayName)}\";"); + sb.AppendLine(); + sb.AppendLine($" /// The description of the extension."); + sb.AppendLine($" public const string Description = \"{EscapeString(metadata.Description)}\";"); + sb.AppendLine(); + sb.AppendLine($" /// URL for more information about the extension."); + sb.AppendLine($" public const string MoreInfo = \"{EscapeString(metadata.MoreInfo)}\";"); + sb.AppendLine(); + sb.AppendLine($" /// Path to the license file."); + sb.AppendLine($" public const string License = \"{EscapeString(metadata.License)}\";"); + sb.AppendLine(); + sb.AppendLine($" /// URL to the getting started guide."); + sb.AppendLine($" public const string GettingStartedGuide = \"{EscapeString(metadata.GettingStartedGuide)}\";"); + sb.AppendLine(); + sb.AppendLine($" /// URL or path to release notes."); + sb.AppendLine($" public const string ReleaseNotes = \"{EscapeString(metadata.ReleaseNotes)}\";"); + sb.AppendLine(); + sb.AppendLine($" /// Path to the extension icon."); + sb.AppendLine($" public const string Icon = \"{EscapeString(metadata.Icon)}\";"); + sb.AppendLine(); + sb.AppendLine($" /// Path to the preview image."); + sb.AppendLine($" public const string PreviewImage = \"{EscapeString(metadata.PreviewImage)}\";"); + sb.AppendLine(); + sb.AppendLine($" /// Comma-separated tags for the extension."); + sb.AppendLine($" public const string Tags = \"{EscapeString(metadata.Tags)}\";"); + sb.AppendLine(); + sb.AppendLine($" /// Whether the extension is marked as a preview release."); + sb.AppendLine($" public const bool IsPreview = {(metadata.IsPreview ? "true" : "false")};"); + sb.AppendLine(" }"); + sb.AppendLine("}"); + + return sb.ToString(); + } + + private static string EscapeString(string? value) + { + if (string.IsNullOrEmpty(value)) return string.Empty; + return value!.Replace("\\", "\\\\").Replace("\"", "\\\""); + } + + private class VsixMetadata + { + public string? Id { get; set; } + public string? Version { get; set; } + public string? Language { get; set; } + public string? Publisher { get; set; } + public string? DisplayName { get; set; } + public string? Description { get; set; } + public string? MoreInfo { get; set; } + public string? License { get; set; } + public string? GettingStartedGuide { get; set; } + public string? ReleaseNotes { get; set; } + public string? Icon { get; set; } + public string? PreviewImage { get; set; } + public string? Tags { get; set; } + public bool IsPreview { get; set; } + } +} diff --git a/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/.template.config/template.json b/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/.template.config/template.json index 09ef7ab..cd62d93 100644 --- a/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/.template.config/template.json +++ b/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/.template.config/template.json @@ -12,25 +12,56 @@ "description": "A Visual Studio extension project using SDK-style format with CodingWithCalvin.VsixSdk", "tags": { "language": "C#", - "type": "project" + "type": "solution" }, "sourceName": "VsixExtension", "preferNameDirectory": true, "defaultName": "VsixExtension", "symbols": { + "extensionName": { + "type": "parameter", + "datatype": "string", + "displayName": "Extension Name", + "description": "The display name shown in Visual Studio's extension manager (defaults to project name)", + "defaultValue": "", + "replaces": "TEMPLATE_EXTENSION_NAME", + "isRequired": false + }, "publisher": { "type": "parameter", "datatype": "string", + "displayName": "Publisher", + "description": "Publisher name for the VSIX manifest (shown in extension manager)", "defaultValue": "MyPublisher", "replaces": "TEMPLATE_PUBLISHER", - "description": "Publisher name for the VSIX manifest" + "isRequired": false }, "description": { "type": "parameter", "datatype": "string", + "displayName": "Description", + "description": "Description of what your extension does", "defaultValue": "A Visual Studio extension", "replaces": "TEMPLATE_DESCRIPTION", - "description": "Description for the extension" + "isRequired": false + }, + "tags": { + "type": "parameter", + "datatype": "string", + "displayName": "Tags", + "description": "Comma-separated tags for discoverability (e.g., productivity, editor)", + "defaultValue": "extension", + "replaces": "TEMPLATE_TAGS", + "isRequired": false + }, + "extensionNameResolved": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "extensionName", + "fallbackVariableName": "name" + }, + "replaces": "TEMPLATE_EXTENSION_NAME_RESOLVED" }, "guid1": { "type": "generated", @@ -65,7 +96,7 @@ ], "primaryOutputs": [ { - "path": "VsixExtension.csproj" + "path": "VsixExtension.slnx" } ], "postActions": [ diff --git a/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/README.md b/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/README.md new file mode 100644 index 0000000..8bd286e --- /dev/null +++ b/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/README.md @@ -0,0 +1,31 @@ +# TEMPLATE_EXTENSION_NAME_RESOLVED + +TEMPLATE_DESCRIPTION + +## Features + +- Feature 1 +- Feature 2 + +## Installation + +1. Download from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/) +2. Or install directly from Visual Studio: **Extensions > Manage Extensions** + +## Usage + +Describe how to use your extension here. + +## Requirements + +- Visual Studio 2022 or later + +## Release Notes + +### 1.0.0 + +- Initial release + +## License + +MIT diff --git a/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/VsixExtension.csproj b/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/VsixExtension.csproj index 61d4d5e..9d1baa9 100644 --- a/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/VsixExtension.csproj +++ b/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/VsixExtension.csproj @@ -2,6 +2,10 @@ net472 + Debug;Release + AnyCPU + AnyCPU + AnyCPU 1.0.0 VsixExtension VsixExtension diff --git a/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/VsixExtension.slnx b/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/VsixExtension.slnx new file mode 100644 index 0000000..acc0de5 --- /dev/null +++ b/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/VsixExtension.slnx @@ -0,0 +1,3 @@ + + + diff --git a/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/source.extension.vsixmanifest b/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/source.extension.vsixmanifest index ac40876..710ead2 100644 --- a/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/source.extension.vsixmanifest +++ b/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/source.extension.vsixmanifest @@ -1,10 +1,10 @@ - - VsixExtension + + TEMPLATE_EXTENSION_NAME_RESOLVED TEMPLATE_DESCRIPTION - extension + TEMPLATE_TAGS diff --git a/src/CodingWithCalvin.VsixSdk.slnx b/src/CodingWithCalvin.VsixSdk.slnx index 9c90ad1..2ca754e 100644 --- a/src/CodingWithCalvin.VsixSdk.slnx +++ b/src/CodingWithCalvin.VsixSdk.slnx @@ -1,4 +1,5 @@ + diff --git a/src/CodingWithCalvin.VsixSdk/CodingWithCalvin.VsixSdk.csproj b/src/CodingWithCalvin.VsixSdk/CodingWithCalvin.VsixSdk.csproj index 5ceb7b9..f2b8353 100644 --- a/src/CodingWithCalvin.VsixSdk/CodingWithCalvin.VsixSdk.csproj +++ b/src/CodingWithCalvin.VsixSdk/CodingWithCalvin.VsixSdk.csproj @@ -34,11 +34,26 @@ false + + + + false + true + all + + + + + + diff --git a/src/CodingWithCalvin.VsixSdk/Sdk/Sdk.Vsix.props b/src/CodingWithCalvin.VsixSdk/Sdk/Sdk.Vsix.props index 05baaa2..cd826f1 100644 --- a/src/CodingWithCalvin.VsixSdk/Sdk/Sdk.Vsix.props +++ b/src/CodingWithCalvin.VsixSdk/Sdk/Sdk.Vsix.props @@ -9,9 +9,12 @@ true - - x64 - x64 + + VsixInfo + + + AnyCPU + AnyCPU net472 @@ -43,6 +46,19 @@ disable + + + true + $(MSBuildProjectDirectory)\Generated + + + $(DefaultItemExcludes);Generated\** @@ -62,6 +78,25 @@ 1.0.0 + + + + true + + + README.md + + + true + + - - - Designer - PreserveNewest - - + + + <_SourceVsixManifestPath Condition="'$(_SourceVsixManifestPath)' == '' and Exists('$(MSBuildProjectDirectory)\source.extension.vsixmanifest')">$(MSBuildProjectDirectory)\source.extension.vsixmanifest + <_SourceVsixManifestPath Condition="'$(_SourceVsixManifestPath)' == '' and Exists('$(MSBuildProjectDirectory)\$(MSBuildProjectName).vsixmanifest')">$(MSBuildProjectDirectory)\$(MSBuildProjectName).vsixmanifest + + - + + + + + + + + + + Designer + + + + + + <_VsixVersionSentinel>$(IntermediateOutputPath)_vsix_version_build.sentinel + + + + - $(Version) - 1.0.0 + <_VsixNsForPoke><Namespace Prefix='vsix' Uri='http://schemas.microsoft.com/developer/vsx-schema/2011'/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_VsixNsForPeek><Namespace Prefix='vsix' Uri='http://schemas.microsoft.com/developer/vsx-schema/2011'/> + + + + + + + + + + + + + + + + + + + + + <_VsixTagsNormalized>$(_VsixTags.Replace(', ', ',')) + <_CategoriesJson>$(_VsixTagsNormalized.Replace(',', '", "')) + <_CategoriesJson>"$(_CategoriesJson)" + <_QnAValue Condition="'$(VsixPublishQnA)' == 'true'">true + <_QnAValue Condition="'$(VsixPublishQnA)' != 'true'">false + <_RepoLine Condition="'$(_VsixMoreInfo)' != ''">, + "repo": "$(_VsixMoreInfo)" + <_RepoLine Condition="'$(_VsixMoreInfo)' == ''"> + + + + + <_PublishManifestContent> + + + + + + Text="Visual Studio 2022 is 64-bit. Consider changing PlatformTarget to 'AnyCPU'." /> + From 76ed02e9ea9d03a29115ce324e267507bcf6740c Mon Sep 17 00:00:00 2001 From: "Calvin A. Allen" Date: Tue, 30 Dec 2025 15:00:39 -0500 Subject: [PATCH 2/3] fix(ci): update restore path to src solution --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa294f3..fda9048 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: dotnet-version: 8.0.x - name: Restore dependencies - run: dotnet restore + run: dotnet restore src/CodingWithCalvin.VsixSdk.slnx - name: Build SDK run: dotnet build src/CodingWithCalvin.VsixSdk/CodingWithCalvin.VsixSdk.csproj -c Release --no-restore From ee8b552421bb0aa51a148c46cd360fd7d3accb15 Mon Sep 17 00:00:00 2001 From: "Calvin A. Allen" Date: Tue, 30 Dec 2025 15:06:21 -0500 Subject: [PATCH 3/3] fix(ci): build projects individually instead of solution restore --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fda9048..5e1b458 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,14 +19,14 @@ jobs: with: dotnet-version: 8.0.x - - name: Restore dependencies - run: dotnet restore src/CodingWithCalvin.VsixSdk.slnx + - name: Create artifacts directory + run: New-Item -ItemType Directory -Force -Path artifacts/packages - name: Build SDK - run: dotnet build src/CodingWithCalvin.VsixSdk/CodingWithCalvin.VsixSdk.csproj -c Release --no-restore + run: dotnet build src/CodingWithCalvin.VsixSdk/CodingWithCalvin.VsixSdk.csproj -c Release - name: Build Templates - run: dotnet pack src/CodingWithCalvin.VsixSdk.Templates/CodingWithCalvin.VsixSdk.Templates.csproj -c Release --no-restore + run: dotnet pack src/CodingWithCalvin.VsixSdk.Templates/CodingWithCalvin.VsixSdk.Templates.csproj -c Release - name: Build Sample Extension run: dotnet build samples/SampleExtension/SampleExtension.csproj -c Release