Créer un générateur de fichier dans VS2017 en utilisant les VSIX

Créer un générateur de fichier dans VS2017 en utilisant les VSIX

2018-10-18 1 By Sauleil

Cet article a pour but de vous aider à créer votre propre génerateur de fichier.

Qu’est-ce que c’est au juste un générateur de fichier?

Parfois dans nos projets, nous avons des fichiers sur lesquels nous devons pré-processer pour avoir un fichier intermédiaire. On peut utiliser un external tool ou utiliser un “Post Build Process” mais ce n’est pas aussi bien intégré. Un bon exemple sont les fichiers “.resx”; ce sont des fichiers de ressources en XML qui une fois générée nous donne comme output un fichier “.Designer.cs”. Et à chaque fois que vous faites une modification, le fichier est généré de nouveau.

Si vous utilisez le clique droit et que vous faites “properties” sur le fichier “resx” en question, vous verrez dans la section “Advanced” le “Custom Tool suivant: “ResXFileCodeGenerator”. C’est cette commande qui est lancé à chaque fois que Visual Studio détecte un changement à votre fichier “.resx”.

Donc la question qui m’a poussé à écrire cet article est: “Comment fait-on pour s’en faire un personnalisé?”

Création d’un générateur pour minifier du Javascript

Cet article suivra les étapes pour créer un “custom tool” pour minifier du javascript. Cet example utilisera aussi une librairie externe en utilisant NuGet. Comme c’est un cas qui arrive souvent, je pense que c’est important de l’inclure dans cet article. Qui plus est, ce n’est pas trivial.

Création d’un projet VSIX

Tout d’abord, je ne connaissais pas ce genre de type de projet avant de commencer à m’intéresser à faire une extension. Auparavant, j’utilisais soit Wix ou l’installeur qui venait avec Visual Studio pour les faire. C’était fastidieux. Donc après avoir lu sur les VSIX, je suis suis dit: pourquoi pas l’essayer.

Dans la documentation de Microsoft, quand on lit, ça semble très simple: vous avez juste à implémenter l’interface IVsSingleFileGenerator. Après avoir cherché un peu sur comment procéder sur l’internet, j’ai découvert qu’on peut utiliser les VSIX. Donc, voici sans plus attendre comment créer votre premier projet.

Requis: Visual Studio SDK

Avant de commencer, soyez sûr que vous avez d’installé le Visual Studio SDK. C’est ce qui va vous permettre de débugger et d’avoir accès au type de projet d’extension VSIX. Pour l’ajouter, vous devrez partir l’installateur de Visual Studio (Visual Studio Installer) et aller ajouter la section SDK

Étapes:

  1. Ouvez Visual Studio 2017
  2. Faites Un nouveau projet de type “VSIX Project”. Ça se retrouve dans la section “Extensibility”. Pour l’exemple, appelons le “MinifyJs“.
  3. Une fois le projet créé, ajoutez un package d’extensibilité au projet: Add -> New Item -> Extensibility -> Visual Studio Async Package. Appelons-le aussi “MinifyJs“.

Une fois ces étapes terminées, vous verrez qu’il y a quelques fichiers qui ont été ajouté. Le fichier qui nous intéresse pour l’instant est “MinifyJs.cs“. Concentrons-nous sur ça et nous reviendrons sur le ménage un peu plus tard.

Implémentation de l’interface IVsSingleFileGenerator

Dans le fichier “MinifyJs.cs“, vous verrez que la classe dérive de la class “AsyncPackage“. Donc voici les étapes à faire pour corriger la situation:

  1. Effacez-tout ce qu’il y a dans la classe à l’exception peut-être de la constante “PackageGuidString“.
  2. Remplacez l’implémentation de l’interface “AsyncPackage” par celle qui nous intéresse: “IVsSingleFileGenerator“.
  3. Ajustez les attributs de la classe (voir dans l’exemple de code ci-bas)
    • Si vous vous demandez d’où vient le GUID dans l’attribut CodeGeneratorRegistration, ça vient de la doc ici.
    • N’oubliez pas d’ajouter les attributs “ComVisible” et le “ProvideObject”.
  4. Ajoutez les 2 méthodes à implémenter.
    • “DefaultExtension” -> Mettez l’extension désiré lors de la conversion du fichier: mettons “.min.js
    • “Generate” -> C’est ici que vous allez faire votre processing. Pour maintenant, on va retourner “Hello World!”
using System;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;

namespace MinifyJs
{
    [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
    [InstalledProductRegistration(nameof(MinifyJs), "Minify a javascript file", "1.0")]
    [Guid(PackageGuidString)]
    [ComVisible(true)]
    [ProvideObject(typeof(MinifyJs))]
    [CodeGeneratorRegistration(typeof(MinifyJs), nameof(MinifyJs), "{FAE04EC1-301F-11D3-BF4B-00C04F79EFBC}", GeneratesDesignTimeSource = true)]
    public sealed class MinifyJs : IVsSingleFileGenerator
    {
        public const string PackageGuidString = "42c44934-b969-4adf-a5d6-f294b05cf00f";

        public int DefaultExtension(out string pbstrDefaultExtension)
        {
            pbstrDefaultExtension = ".min.js";
            return VSConstants.S_OK;
        }

        public int Generate(string wszInputFilePath, string bstrInputFileContents, string wszDefaultNamespace,
            IntPtr[] rgbOutputFileContents, out uint pcbOutput, IVsGeneratorProgress pGenerateProgress)
        {
            try
            {
                var bytes = Encoding.UTF8.GetBytes("Hello World!");
                var length = bytes.Length;
                rgbOutputFileContents[0] = Marshal.AllocCoTaskMem(length);
                Marshal.Copy(bytes, 0, rgbOutputFileContents[0], length);
                pcbOutput = (uint)length;
            }
            catch (Exception ex)
            {
                ThreadHelper.ThrowIfNotOnUIThread();
                pGenerateProgress.GeneratorError(0, 0, ex.Message, 0, 0);
                pcbOutput = 0;
            }

            return VSConstants.S_OK;
        }
    }
}

à Partir de ça, vous devriez avoir votre premier VSIX de complété et vous pouvez l’essayer!

Tester et Débugger un VSIX

Ce qui est cool avec ce type de projet est la facilité à tester et débugger. Mais avant d’aller plus loin, nous devons faire une modification au fichier de config pour être sûr de bien supporter toutes les versions de Visual Studio:

  1. Ouvrez “source.extension.vsixmanifest
  2. Cliquez sur “Install Targets“. Vous verrez qu’il y a probablement une seule target “Microsof.VisualStudio.Community“.
  3. Ajoutons les autres distributions de visual studio en faisant “New” (voir image plus bas)
  4. Faites F5 (Run and Debug) et ça va lancer votre Visual Studio Expérimental
  5. Vous allez devoir vous créer un nouveau projet (type console fera l’affaire).
  6. Dans ce projet, créez un fichier “Test.js” et ajouter quelques lignes de code javascript.
  7. Dans le “Solution Explorer“, allez chercher les propriétés du Fichier “Test.cs
  8. Mettez dans la propriété “custom tool” le nom que vous avez enregistré dans l’attribut “CodeGeneratorRegistration“; dans notre cas “MinifyJs” (voir image plus bas)
  9. Après avoir terminé l’édition, vous devriez voir le fichier généré avec le texte “Hello World!” dedans.
  10. Vous pouvez maintenant fermer le VS Expérimental.

 
Normalement, ce tutorial pourrait s’arrêté ici, mais allors un peu plus loin.

Intégration d’un package NuGet dans un VSIX

L’ajout d’un package NuGet se fait comme d’habitude. Le problème est plutôt quand on arrive au déploiement.

Donc, pour notre exemple, nous allons intégrer la librairie de Yahoo pour compresser notre code Javascript.

  1. Premièrement, ouvrez le dialogue de NuGet: “Tools -> Nuget Package Manager -> Manage NuGet packages for Solution…
  2. Cliquez sur l’onglet “Browse
  3. Tapez “YUICompressor.NET” et installez le dans votre projet

Et voilà, vous avez maintenant les dépendances dans votre projet. Maintenant, allons coder la fonction “Generate” avec le vrai code:

var compressor = new JavaScriptCompressor
{
    DisableOptimizations = false,
    ObfuscateJavascript = true
};

var compressedFile = compressor.Compress(bstrInputFileContents);
var bytes = Encoding.UTF8.GetBytes(compressedFile);

Si vous essayez de partir le projet, ça ne fonctionnera pas. Les dépendances ne seront pas “bundler” avec. Il y a 2 méthodes que je connais pour y arriver.

Assets

La première est la plus simple, mais n’est pas recommandé dans ce cas. On clique sur le fichier “source.extension.vsixmanifest“, on va dans “Assets” et on ajoute les dll manuellement.

Le problème majeur est que Visual Studio va copier les fichiers en local dans le projet… et que si vous faites des updates des NuGet packages, vous allez devoir recopier à la main ces dll à chaque fois. Sinon l’application déployée ne fonctionnera pas, dû aux link et aux binaires qui ne correspondront pas.

Ajout d’Attribut dans le projet

Selon moi c’est la meilleure méthode que j’ai trouvé. Donc, voici les étapes:

  1. Cliquez droit sur le projet dans le “Solution Explorer” et cliquez sur”Unload Project“.
  2. Cliquez droit encore sur le projet et faite “Edit
  3. Chechez les 2 références “EcmaScript.NET” et “Yahoo.Yui.Compressor“, qui ont été ajouté par NuGet.
  4. Ajoutez “<ForceIncludeInVSIX>True</ForceIncludeInVSIX>” comme propriété.
  5. Cliquez droit encore sur le projet et faire Reload après avoir sauvegardé.
<Reference Include="EcmaScript.NET, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
      <HintPath>..\packages\EcmaScript.NET.2.0.0\lib\net45\EcmaScript.NET.dll</HintPath>
      <ForceIncludeInVSIX>True</ForceIncludeInVSIX>
</Reference>
<Reference Include="Yahoo.Yui.Compressor, Version=3.0.0.0, Culture=neutral, processorArchitecture=MSIL">
      <HintPath>..\packages\YUICompressor.NET.3.0.0\lib\net452\Yahoo.Yui.Compressor.dll</HintPath>
      <ForceIncludeInVSIX>True</ForceIncludeInVSIX>
</Reference>

Référence: http://comealive.io/Forcing-DLLs-To-Vsix/

Dans le meilleur des mondes, vous auriez juste à faire F5 et tout fonctionnerait parfaitement. Mais non! Cette erreur m’a prise plusieurs heures pour bien comprendre et la régler:

Je n’irai pas dans les détails, mais le problème vient du fait qu’on signe notre “package” et que les dll du NuGet ne le sont pas. Donc, 2 options s’ouvrent à nous: signer manuellement les dll externes ou bien simplement désactiver notre signature.

Ce n’est pas très réaliste de vouloir signer les dll externes, alors nous allons simplement désactiver la signature dans les propriétés de notre projet. Voici un screenshot pour vous aider.

Donc, si vous lancez votre application, vous allez pouvoir faire fonctionner votre minimifieur Javascript.

Installation

Une fois que vous êtes satisfait de votre extension, faite une version release (ou debug) et partagez votre VSIX. Les développeurs auront juste à double cliquer dessus.

Dans les settings du package j’ai vu qu’on pouvait générer un installeur windows, donc j’imagine qu’on peut le distribuer de cette méthode aussi mais je n’ai pas essayé.

Ménage

Avec tout ça, il y a quelques fichiers que nous n’avons plus vraiment de besoin.

  • Index.html – C’est juste de la documentation générique. Je préfère les README.md
  • stylesheet.css – C’est le fichier de style avec l’index.
  • Key.snk – on a désactivé la signature, donc on n’en a plus vraiment de besoin

Trucs et Astuces

  • Une fois que le “Custom Tool” est assigné, vous pouvez manuellement à tout moment lancer la génération via le “custom tool” en cliquant droit sur le fichier et choisissant “Run custom tool“.
  • Dans la fonction “Generate“, il y a un paramètre “IVsGeneratorProgress pGenerateProgress“. Vous pouvez vous en servir pour écrire des messages d’erreurs mais aussi de spécifier le progrès si jamais votre processus est très long.
  • Pour voir si votre extension est installée (dans l’expérimental ou non), juste aller dans: “Tool -> Extensions and Updates“. Vous pouvez la désinstaller de là aussi.
  • Dans la version Expérimentale, les fichiers sont copié à cet endroit: “%USERPROFILE%\AppData\Local\Microsoft\VisualStudio\15.0_<xxx>Exp\Extensions\<USERNAME>\MinifyJs“. Ça peut être pratique pour voir exactement ce qui est copié si jamais vous avez des problèmes de dépendances.