One of my targets of this year was to start writing TypeScript and experience the usability compared to my experience writing javascript over 20 years ago. My weapon of choice to write build and release extensions have always been PowerShell and it has always gotten the job done. Until recently I wanted to expose more clearly to a team a “state” a release pipeline is in, more about this topic in the near future. But this forced me to start thinking towards TypeScript to expose the information like a hub inside the VSTS Release hub.
The first thing I experienced was that the documentation about this topic is kind of scattered and you need to keep in mind that there are multiple ways and versions to achieve it. This cost me a lot of time because packages and translations didn’t always work as I expected. Which resulted in the idea to write a blog about this topic to help people kickstart writing the extension.
TLDR;
If you want the working example just get them from my git repo here. And enter npm install , tsc -p . .Change the “publisher”: “publisher name”, to your publisher name and enter tfx extension create –rev-version and publish your extension to your VSTS.
Or you can use the generator-vsts-extension here, developed by the Alm Rangers.
npm install -g generator-team-services-extension yo team-services-extension
Getting ready
To get started you’ll need:
- A VSTS account if you haven’t got one you can start here
- A text editor. I used and love Visual Studio Code, and you can download it for free here
- The latest version of node which can be found here
Getting the environment ready
Fire up Visual Studio Code and open a new folder let’s call it home, make sure to use only lowercase. This will make the npm
setup easier later on. Inside home we are going to create the following project structure:
|-- src
|-- index.ts
|-- static
|-- css
|-- index.css
|-- images
|-- logo.png
|-- index.html
|-- vss-extension.json
|-- package.json
|-- tsconfig.json
You can achieve this by running the following commands from your project root directory home in the “Integrated Terminal” window Ctrl+~.
md src md static new-item static\index.html md static\css md static\images new-item src\index.ts new-item static\images\logo.png new-item vss-extension.json new-item README.md
Getting dependencies and configuration ready
To manage the dependencies we use npm which was installed during the node installation. There are different ways to install the dependencies depending on the use of your project. During this walktrough, we use the –save for the packages your code is dependent on during run-time. And we use –save-dev for the packages dependent on during compile time. Run the following statements in the “Integrated Terminal” window in the project root directory home.
#Initialize the npm with the default values, this will add your package.json (--yes accepts all the default values) npm init --yes #Install the vss-web-extension-sdk, @types/jquery, @types/q in the project dependencies, this will install the sdk in node_modules npm install @types/jquery --save npm install @types/q --save npm install vss-web-extension-sdk --save #Install the typescript, tfx-cli in the developer dependencies npm install typescript --save-dev npm install tfx-cli --save-dev #initialize typscript, this will create your tsconfig.json tsc --init
Next, we need to tell the typescript how to use our code and where to find the type definitions of the extension we want to use. So change the settings in the tsconfig.json to resemble these:
{ "compilerOptions": { "module": "amd", "moduleResolution": "node", "target": "es5", "rootDir": "src/", "outDir": "dist/", "types": [ "vss-web-extension-sdk" ] } }
Now we can compile our code using tsc -p . this will create a dist/index.js.
To publish the extension to VSTS we first need to package it. Therefore we installed the tfx-cli
. This will take care of that after we correctly entered the manifest in the vss-extension.json. A few interesting parts of the manifest are the scopes . Through them, you can manage the permissions of your application it needs to run. You can find the full list here. Another tricky setting that took me long to figure out was the “includes”: [“ms.vss-releaseManagement-web.release-service-data-external”], which is needed on top of the scopes for the release management rest API.
{ "manifestVersion": 1, "id": "first-extension", "version": "0.1.1", "name": "First extension", "description": "An extension.", "publisher": "publisher name", "targets": [ { "id": "Microsoft.VisualStudio.Services" } ], "icons": { "default": "static/images/logo.png" }, "contributions": [ { "id": "Publishername.Extensions.First", "type": "ms.vss-web.hub", "includes": ["ms.vss-releaseManagement-web.release-service-data-external"], "description": "Adds a 'First' hub to the release hub group.", "targets": [ "ms.vss-build-web.build-release-hub-group" ], "properties": { "name": "Release", "order": 99, "uri": "static/index.html" } } ], "scopes": [ "vso.work", "vso.build_execute", "vso.release_manage" ], "files": [ { "path": "dist", "addressable": true }, { "path": "static", "addressable": true }, { "path": "node_modules/vss-web-extension-sdk/lib", "addressable": true, "packagePath": "lib" } ] }
Setting up the index.html
To create an index.html in the static folder we need 3 things:
- A VSS.init()
- some containers to show the data we are going to load
- VSS.require() where we tell to call our index.js and load some data through the API.
<head> <script src="../lib/VSS.SDK.min.js"></script> </head> <body> <script type="text/javascript"> // Initialize the VSS sdk VSS.init({ usePlatformScripts: true, usePlatformStyles: true });</script> <p id="builds">Loading...</p> <p id="workitems">Loading...</p> <div id="grid-container"></div> <script type="text/javascript"> VSS.require(["dist/index"], function (app) { }); </script> </body>
Write your typescript to query the REST API.
The reason you want to write extensions is to interact with the already available data from VSTS. Therefore we have the REST API’s who we can query. First, we need to get the REST client for the specific API, then we can query it and put the result JSON on screen or in any other UI Control you want.
Before we create the UI you can add the following code to your index.ts . It will allow your typing to be recognized in your IDE and give a basic function I used to show the queried JSON on screen. When you have other libraries or you didn’t set them in your tsconfig.json under “types: [“”]” . You can always reference to them by adding the reference like this /// <reference types=”vss-web-extension-sdk” /> on the top of your index.ts
The code below is some helper functions.
export function getTeamContext(){ var webcontext = VSS.getWebContext(); return { projectname : webcontext.project.name, teamId: webcontext.team.id }; } export function show(divName: string, func: (target: HTMLElement) => void){ const elt = document.getElementById(divName)!; let result = func(elt); }
Next, you can query the API and show the JSON on screen for example:
To get all builds of your project:
import BuildRestClient = require("TFS/Build/RestClient"); export function getAvailableBuildDefinitions(target: HTMLElement): void { // Get an instance of the client let client = BuildRestClient.getClient(); client.getDefinitions(getTeamContext().projectname).then(definitions => { target.innerText = JSON.stringify(definitions) } ); } show("builds", getAvailableBuildDefinitions);
Some specific workitems:
import WorkitemRestClient = require("TFS/WorkItemTracking/RestClient"); export function geWorkitems(target: HTMLElement): void { // Get an instance of the client let client = WorkitemRestClient.getClient(); client.getWorkItems([1,2,3,4,5,6,7,8,9,10,272]).then(definitions => { target.innerText = JSON.stringify(definitions) } ); } show("workitems", geWorkitems);
The JSON on screen gives you an idea of the data available to work with. But it doesn’t look that nice, does it? So now we can try to get all the release definition data and show it in a grid on the screen.
import RestClient = require("ReleaseManagement/Core/RestClient"); import Controls = require("VSS/Controls"); import Grids = require("VSS/Controls/Grids"); //little holder class for my grid datasource class releaseGrid{ name: string; id: number; } export function getAvailableReleaseDefinitions(source: Array<releaseGrid>, target: Grids.Grid): void { // Get an instance of the client let client = RestClient.getClient(); client.getReleaseDefinitions(getTeamContext().projectname).then(definitions => { definitions.forEach(d => { source.push({ name: d.name, id: d.id }); }) //data is retrieved via a IPromise so update the datasource when you have gotten it target.setDataSource(source); } ); } //get the div to show your grid var container = $("#grid-container"); var source = new Array<releaseGrid>(); //define your grid var gridOptions: Grids.IGridOptions = { height: "300px", width: "500px", source: source, columns: [ { text: "ReleaseName", width: 200, index: "name" }, { text: "ReleaseIdentifier", width: 200, index: "id" } ] }; var grid = Controls.create(Grids.Grid, container, gridOptions); getAvailableReleaseDefinitions(source, grid);
When you compile your typescript tsc -p . . Remember that even though you can experience compilation errors your .js will always be built and you can try anyhow.
node_modules/@types/jquery/index.d.ts(2961,63): error TS2304: Cannot find name 'Iterable'. node_modules/vss-web-extension-sdk/typings/vss.d.ts(544,5): error TS7010: 'removeChannel', which lacks return-type annotation, implicitly has an 'any' return type. node_modules/vss-web-extension-sdk/typings/vss.d.ts(833,5): error TS7010: 'close', which lacks return-type annotation, implicitly has an 'any' return type. node_modules/vss-web-extension-sdk/typings/vss.d.ts(840,5): error TS7010: 'setTitle', which lacks return-type annotation, implicitly has an 'any' return type. node_modules/vss-web-extension-sdk/typings/vss.d.ts(845,5): error TS7010: 'updateOkButton', which lacks return-type annotation, implicitly has an 'any' return type. node_modules/vss-web-extension-sdk/typings/vss.d.ts(968,5): error TS7010: 'reload', which lacks return-type annotation, implicitly has an 'any' return type. node_modules/vss-web-extension-sdk/typings/vss.d.ts(975,5): error TS7010: 'onHashChanged', which lacks return-type annotation, implicitly has an 'any' return type. node_modules/vss-web-extension-sdk/typings/vss.d.ts(989,5): error TS7010: 'setHash', which lacks return-type annotation, implicitly has an 'any' return type. node_modules/vss-web-extension-sdk/typings/vss.d.ts(1110,5): error TS7010: 'execute', which lacks return-type annotation, implicitly has an 'any' return type. node_modules/vss-web-extension-sdk/typings/vss.d.ts(12209,30): error TS7006: Parameter 'IGridColumn' implicitly has an 'any' type. node_modules/vss-web-extension-sdk/typings/vss.d.ts(35172,76): error TS7006: Parameter 'args' implicitly has an 'any' type.
Create your package to publish
Before we can package the extension we need to replace the static\images\logo.png with a nice picture for your extension. In the vss-extension.json you need to set the “publisher”: “PublisherName” to the same name you registered with. Now your code is ready for your first publish you need to run the tfx extension create you can add the –rev-version to auto increment your version number. Upload it to your marketplace account and share it with your vsts account.
When you now go to your manage extensions tab you should see the “First extension” and when you click on it you can install it.
Confirm the permissions and check out your extension in the release tab. Out of the box, this should work and any problems you can debug using F12 in your browser.
All that remains is your own creativity to get a great idea, program it and release it to everybody to enjoy. When you are through the pain of setting up your typescript config I really like the typescript experience compared to the direct javascript but that is for everybody to decide.
Here is the Git repo I used.
Here you can find the Extension Manifest References
Here you can find the Rest Client documentation
Have fun coding!
Hi Erick,
Thank you so much for the blog. This is what I was looking for :-). Thanks for sharing the information. I was also struggling a lot while creating extension for VSTS. I am very confident now that I can proceed further from here.
Your welcome