銀の光と碧い空

クラウドなインフラとC#なアプリ開発の狭間にいるエンジニアの日々

VSTS 拡張を作ってみる (6) : TypeScriptにする

Visual Studio Advent Calendar のトリを務めることになりました。

qiita.com

また、人知れず(実は)やっていた一人Advent Calendarも完走しました。

www.adventar.org

本題のVSTS拡張の方ですが、ここらでTypeScriptで書き換えてみたいと思います。

TypeScriptについてはまじめに書くのが初めてな状態ですが、まず、TypeScriptはVisual Studio Codeで開発することにして、このあたりのページを参考に環境を整えました。

https://code.visualstudio.com/Docs/languages/typescript

qiita.com

さて、VSTS拡張をTypeScriptで書く方法ですが、これまた最新の情報は整理されきってはいないようです。情報が古いと言っていたサンプルリポジトリにTypeScriptで書かれたサンプルがあるのでそれを参考にしつつ、

github.com

公式ページのサンプルコードのいくつかはTypeScriptでも書かれているので、それらを参考にとりあえず動くものが作れました。

Overview of extensions for Visual Studio Team Services and Team Foundation Server

まず、フォルダ構造を説明します。おそらく変えてもいい場所もありますが、いったんこれで動かしているということで。

  • .vdcode (VSCode用の設定フォルダ)
    • tasks.json (VSCodeでTypeScriptをビルドするときのタスク定義ファイル)
  • out/scripts (ビルドして生成するJavaScriptの出力先フォルダ)
  • scripts (TypeScriptファイルの保存フォルダ)
    • main.ts (今回記述するTypeScriptファイル)
  • sdk/scripts/VSS.SDK.js (VSS SDKのライブラリ)
  • typings (参照するライブラリのTypeScriptの型定義(d.ts)ファイル。vssとtfsはVSS SDKのGitHubリポジトリから、そのほかは適当な場所から持ってくる)
    • jquery/jquery.d.ts
    • knockout/knockout.d.ts
    • q/q.d.ts
    • tfs.d.ts
    • vss.d.ts
  • build.html (VSTS拡張で書くHTMLファイル)
  • tsconfig.json (TypeScriptのビルドオプションファイル)
  • vss-extension.json (VSTS拡張の定義ファイル)

f:id:tanaka733:20151224150132j:plain

tasks.json はこんな感じ

{
    "version": "0.1.0",
    "command": "tsc",
    "isShellCommand": true,
    "showOutput": "silent",
    "args": ["-p", "."],
    "problemMatcher": "$tsc"
}

tsconfig.json はこう。async/await使いたくてes6にしてみたら、VSS.d.ts にエラーが多発したので断念。

{
    "compilerOptions": {
        "module": "amd",
        "target": "es5",
        "outDir": "out/scripts",
        "moduleResolution": "node"
    },
    "files": ["scripts/main.ts"]
}

vss-extension.json は基本的に同じですが、files要素でVSIXに含めるスクリプトはJavaScriptのみ(VSS.SDK.js とコンパイルして出力されたjsファイルのみ)になります。

あとは肝心のTypeScriptですがこんな感じで書いてみました。ところどころ、JavaScript臭がしますが。。。あとこのコード、VSCodeで警告が出ます。どうも必要な上に実際のオブジェクトには存在するプロパティだけど、型定義ファイルには定義されていないものがあったりするようです...

/// <reference path='../typings/vss' />
/// <reference path='../typings/tfs' />

import Controls = require("VSS/Controls");
import Grids = require("VSS/Controls/Grids");
import TFS_Build_Contracts = require("TFS/Build/Contracts");
import VSS_Service = require("VSS/Service");
import TFS_Build_Client = require("TFS/Build/RestClient");

var vsoContext = VSS.getWebContext();
var build = TFS_Build_Client.getClient();
var container = $("#grid-container");
var regex = /^[^-_]+[-_]+([^-_]+)([-_]+|$)/;

build.getDefinitions(vsoContext.project.name).then<Contracts.DefinitionReference[]>((buildDefinitions) =>{
  build.getBuilds(vsoContext.project.name, buildDefinitions.map((x) => {return x.id;})).then<any>((builds) => {
    //grouping
    var groups = new Map<any, any>();
    buildDefinitions.forEach(function(b){
      var m = regex.exec(b.name);
      var n = m == null ? "" : m[1];
      var group = groups.get(n);
      if (group == null){
        group = [];
        groups.set(n, group);
      }
      group.push(b);
    });
    console.log(groups);
    var output = [];
    groups.forEach(function(value, key, m){
      var item: {groupName: string, children: [any]} = {groupName: key, children: value};
      output.push(item);
    });
    console.log(output);
    var gridOptions: Grids.IGridOptions = {
      height: "1000px",
      width: "100%",
      columns: [
          { text: "group", index: "groupName", width: 100, indent: true },
          { text: "ID", index: "id", width: 50 },
          { text: "Name", index: "name", width: 200 },
          { text: "Last Status", width: 50, getColumnValue: (index: number)=>{
            var id = grid.getRowData(index).id;
            //buildNumberRevisionの最新1件を取得。OrderByDesend().FirstOrDefault()
            var filtered = builds.filter(function(e, i, array) {
              return e.definition.id == id;
            }).sort(function(a,b) { 
              if (a.buildNumberRevision < b.buildNumberRevision)
                return 1;
              if (a.buildNumberRevision > b.buildNumberRevision)
                return -1;
              return 0;
            });
            if (filtered == null || filtered.length == 0)
              return "";
            console.log(filtered);
            if (filtered[0].result != 2)
              return "NG";
            return "OK";
          }}
      ],
      // This data source is rendered into the Grid columns defined above
      source: new Grids.GridHierarchySource(output),
      gutter: {
        contextMenu: true
      },
      contextMenu: {
        items: [{
            id: "open",
            text: "Open Details"
          }
        ],
        executeAction: (args) => {
            var buildDefinition = args.get_commandArgument().item;
            switch (args.get_commandName()) {
              case "open":
                window.parent.location.href = `${vsoContext.host.uri}/${vsoContext.project.name}/_build#_a=completed&definitionType=2&definitionId=${buildDefinition.id}`;
                break;
              case "queue":
                break;
              default:
                console.info(args.get_commandName());
            }
        },
        //コンパイルエラーになるけど必要
        arguments: (contextInfo) => {
          return { item: contextInfo.item };
        }
      }
    };
  
    var grid = Controls.create<Grids.Grid, Grids.IGridOptions>(Grids.Grid, container, gridOptions);
    VSS.notifyLoadSucceeded();
  })
});

後は、VSCodeでビルドして、今まで通りVSIXパッケージを作ってアップロードすれば使えます。