VSCodeでTypeScript/Node.jsの開発環境を作る(UT・カバレッジ・ログ出力・リリース手順含む)

Visual Studio CodeでTypeScriptの開発環境を作る手順を調べてみました。
Node.jsで動く簡単なCLIツールを作る前提で進めていきますが、他の開発にも応用できるかと思います。

なお、Visual Studio CodeNode.jsはインストール済みの前提です。
Windows 10環境で以下のバージョンで確認しています。

C:\>node -v
v8.10.0

C:\>npm -v
3.10.6

C:\>code -v
1.21.1
79b44aa704ce542d8ca4a3cc44cfca566e7720f1
x64

(ショートカット)

以降で使っていくコマンドです。(一度にインストールしたい人向け)

mkdir ts-sample
cd ts-sample

npm init -y
mkdir .vscode src test config

npm i -SEB config log4js
npm i -DE typescript ts-node tslint
npm i -DE @types/node @types/config
npm i -DE espower-typescript power-assert mocha @types/mocha
npm i -DE rimraf cpx

node_modules\.bin\tsc --init
npm i -g npm-run
npm-run tslint --init

code --install-extension eg2.tslint
code --install-extension fabiospampinato.vscode-commands

TypeScriptプロジェクトの作成

プロジェクトフォルダー(例:ts-sample)をまず作成して、npm initでpackage.jsonを出力します。

mkdir ts-sample
cd ts-sample
npm init -y
(初回生成)package.json
{
  "name": "ts-sample",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": ""
}

公開しない限り、ほとんどの項目は不要なので適当に削除しておきます。

package.json
{
  "name": "ts-sample",
  "version": "1.0.0",
  "private": true
}

ただし、以下のようなWARNがnpmコマンドを打つたびに出るのもアレなので"private": trueで非公開設定にしておきます。

npm WARN ts-sample@1.0.0 No description
npm WARN ts-sample@1.0.0 No repository field.
npm WARN ts-sample@1.0.0 No license field.

続いて、プロジェクトに必要なディレクトリを作成します。

mkdir .vscode src test config

なお、今回のプロジェクト構成は以下のようになっています。

├─.vscode
├─bin          # リリース後のjsソース配置先
├─build        # ビルド用の一時ディレクトリ
│  ├─src       # jsソース出力先
│  └─test      # jsテスト出力先
├─config   
├─coverage
├─logs
├─node_modules
├─src          # tsソース
└─test         # tsテスト

TypeScriptトランスパイラ(tsc)とTS実行ツール(ts-node)のインストール

以下のコマンドでインストールします。
npm inpm installと同じです。-D--save-dev-E--save-exactと同じで、package.jsonのdevDependenciesにバージョン固定で記録されます)

npm i -DE typescript ts-node 

インストール後、tsc --initでtsconfig.json(トランスパイル設定ファイル)を出力します。

node_modules\.bin\tsc --init 
(初回生成[コメント省略])tsconifg.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true
  }
}

今回はNode.jsで動くCLIツールを作る前提なので、targetはより新しいES2017にしておきます。
また、デバッグ用に"sourceMap": trueと、js出力先outDir、tsソース配置先includeを追加します。

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2017",
    "module": "commonjs",
    "sourceMap": true,
    "outDir": "build",
    "strict": true
  },
  "include": [
    "src/**/*",
    "test/**/*"
  ]
}

なお、esModuleInteropは削除しています。これがtrueになっているとconfigモジュールを使ったときにTypeError: config.get is not a functionというエラーが出たので、現時点では削除しておいたほうがよさそうです。

簡単なソースでお試し実行

src/main.ts
const message: string = "world";
console.log(`Hello ${message}`);

src内に上記のようなtsを作って、tscでトランスパイル(コンパイル)すると

node_modules\.bin\tsc

以下のようなjsが出力されます。
(test内が空だとbuild直下、空でなければbuild/srcに出力されます)

build/main.js
"use strict";
const message = "world";
console.log(`Hello ${message}`);
//# sourceMappingURL=main.js.map

以下のコマンドで実行するとHello worldと出力されると思います。

node build/main.js

なお、ts-nodeを使うとコンパイルせずに実行できます。

node_modules\.bin\ts-node src/main.ts

また、毎回node_modules\.bin\と打つのも大変なので、npm-runをインストールすると楽できます。
(以降の説明でもnpm-runはインストール済みとしています)

npm i -g npm-run
npm-run ts-node src/main.ts

注意点として、Windowsの場合、npm-runは拡張子が.exeのものを先に見てしまうようで、tscはC:\Program Files (x86)\Microsoft SDKs\TypeScript\1.0\tsc.exeが先に見つかってしまうとnpm-runでは使えません。
(コマンドプロンプトからだとwhere tscでtscの場所が見れます)

TSLintのインストール・設定

まずはtslintをインストールします。

npm i -DE tslint

さらにVSCodeでTSLint拡張を画面で選択するか、以下のコマンドでインストールします。

code --install-extension eg2.tslint

以下のコマンドでtslint.jsonを生成すると、lint(コードの静的解析)が有効になります。

npm-run tslint --init
(初回生成)tslint.json
{
    "defaultSeverity": "error",
    "extends": [
        "tslint:recommended"
    ],
    "jsRules": {},
    "rules": {},
    "rulesDirectory": []
}

"defaultSeverity": "error"だとコンパイルエラーとlintのエラーの区別がつかなくなるのでwarnに変更しておきます。
console.logで怒られないようにしたい場合は"no-console": falseを設定します。

tslint.json
{
    "defaultSeverity": "warn",
    "extends": [
        "tslint:recommended"
    ],
    "jsRules": {},
    "rules": {
        "no-console": false
    },
    "rulesDirectory": []
}

VSCodeでコード実行タスクの設定

以下のようにコマンドパレットで選択すると、tasks.jsonが出力されます。

task.gif

(初回生成).vscode/tasks.json
{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "tasks": [
        {
            "label": "echo",
            "type": "shell",
            "command": "echo Hello"
        }
    ]
}

以下のようにするとタスク経由でtsコードの実行ができます。

vscode/tasks.json
{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "ts-node",
            "type": "shell",
            "command": "npm-run ts-node ${relativeFile}"
        }
    ]
}

ユニットテストの実行

Mochapower-assertを使ったユニットテストの設定をしていきます。
まずは以下のコマンドで必要なツールをインストールします。

npm i -DE @types/node
npm i -DE espower-typescript power-assert mocha @types/mocha

以下のようなソースで実行してみます。
if (require.main === module)の中はソースが(テスト経由でなく)直接実行されたときのみ動きます)

src/main.ts
export function main(message: string) {
    console.log(`${message}`);
    return `Hello ${message}`;
}

if (require.main === module) {
    main(process.argv[2]);
}
test/main.test.ts
import assert = require("assert");
import {main} from "../src/main";

describe("main()", () => {
    const message = "world!";
    it("hello world!", () => {
        assert(main(message) === "Hello world");
    });
});

以下のようにmochaのタスクを作成して実行すると

vscode/tasks.json
{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "ts-node",
            "type": "shell",
            "command": "npm-run ts-node ${relativeFile}"
        },
        {
            "label": "mocha",
            "type": "shell",
            "command": "npm-run mocha -r espower-typescript/guess ${relativeFile}"
        }
    ]
}

以下のように詳細なエラー表示がされます。
(上記で${relativeFile}の代わりに${file}を使うとpower-assertが効かなくなるので注意です)

mocha_error.png

(比較対象の右辺の最後に!が足りないのでエラーになっています)

(オプション)ステータスバーにボタン設置

VSCodeにCommands拡張を入れるとステータスバーのボタンから各種実行ができて便利です。
VSCodeの拡張画面からインストールするか、以下のコマンドでインストールできます。

code --install-extension fabiospampinato.vscode-commands

インストール後、コマンドパレットからCommands: Edit Configurationを実行するとcommands.jsonが出力されます。

(初回生成).vscode/commands.json
{
  "commands": [
    {
      "command": "commands.refresh",
      "text": "$(sync)",
      "tooltip": "Refresh commands",
      "color": "#FFCC00"
    }
  ]
}

以下のように設定すると

vscode/commands.json
{
  "commands": [
    {
      "command": "commands.refresh",
      "text": "$(sync)",
      "color": "#FFCC00"
    },
    {
      "command": "workbench.action.tasks.runTask",
      "arguments": ["ts-node"],
      "text": "$(triangle-right) ts-node",
      "color": "#CCFFCC",
      "filterFileRegex": "\\.ts"
    },
    {
      "command": "workbench.action.tasks.runTask",
      "arguments": ["mocha"],
      "text": "$(octoface) mocha",
      "color": "#EEBBAA",
      "filterFileRegex": "\\.ts"
    },
    {
      "command": "workbench.action.terminal.toggleTerminal",
      "text": "$(terminal)"
    }
  ]
}

以下のような表示になります。

statusbar.png

なおtextにはOcticonsが使えます。

テストカバレッジの出力

nycを使うと簡単にテストカバレッジが出力できます。

npm i -DE nyc
(抜粋)package.json
  "scripts": {
    "test": "nyc -i ts-node/register --temp-directory coverage/.nyc -r text -r html -n test/**/*.ts -n src/**/*.ts -e .ts mocha test/**/*.ts"
  },

上記のようにpackage.jsonのscriptsにtestを設定すると、npm testで以下のような出力がされます。

cover_text.png

以下はcoverageディレクトリ内にHTMLで出力されたものです。

cover_html.png

(オプション)デバッグ実行の設定

デバッグ画面で歯車のようなボタンを押すとlaunch.jsonが出力されます。

(初回生成).vscode/launch.json
{
    // IntelliSense を使用して利用可能な属性を学べます。
    // 既存の属性の説明をホバーして表示します。
    // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "プログラムの起動",
            "program": "${workspaceFolder}/src\\main.ts",
            "outFiles": [
                "${workspaceFolder}/**/*.js"
            ]
        }
    ]
}

以下の設定でデバッグ実行ができてブレークポイントなどが使えます。
デバッグコンソールよりもターミナルのほうが、標準出力の表示がされるなど便利なので"console": "integratedTerminal"の設定をしています。

vscode/launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "ts-node[debug]",
            "program": "${workspaceFolder}/node_modules/ts-node/dist/bin.js",
            "args": [
                "${relativeFile}"
            ],
            "console": "integratedTerminal"
        },
        {
            "type": "node",
            "request": "launch",
            "name": "mocha[debug]",
            "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
            "args": [
                "--no-timeouts",
                "--colors",
                "--require",
                "espower-typescript/guess",
                "${relativeFile}"
            ],
            "console": "integratedTerminal"
        }
    ]
}

(VSCode 1.21.1にはバグがあってブレークポイントが効かないことがあります。VSCode Insiders 1.20で上記の動作確認しています)

(オプション)設定ファイルの読み込みとログ出力設定

configで設定ファイルを読み込んで、log4jsでログ出力するサンプルを作ってみました。
以下のコマンドで必要なものをインストールします。
-S--saveと同じでpackage.jsonのdependenciesに記録されます。-B--save-bundleと同じでbundleDependenciesに記録され、パッケージ化した際に内部にライブラリが含まれるようになります)
(なお、@types/log4jsは不要です。log4jsに既に含まれていますので)

npm i -SEB config log4js
npm i -DE @types/config

設定ファイルは標準のもの(default.json)と開発時に値が上書きされるもの(development.json)を作成しています。(詳しくはライブラリのドキュメントを見てください)

config/default.json
{
    "log4js": {
        "appenders": {
            "out": {
                "type": "stdout"
            },
            "error": {
                "type": "dateFile",
                "filename": "logs/error.log",
                "daysToKeep": 31
            }
        },
        "categories": {
            "default": {
                "appenders": [
                    "error"
                ],
                "level": "INFO"
            },
            "console": {
                "appenders": [
                    "out",
                    "error"
                ],
                "level": "INFO"
            }
        }
    }
}
config/development.json
{
    "log4js": {
        "categories": {
            "default": {
                "level": "DEBUG"
            },
            "console": {
                "level": "DEBUG"
            }
        }
    }
}

ソースは以下のとおりです。
#!/usr/bin/env nodeはパッケージ化した後、そのパッケージのインストール時に自動でnode_modules/.binに実行スクリプトが作られるようにするためのものです。

src/main.ts
#!/usr/bin/env node
process.env.NODE_CONFIG_DIR = __dirname + "/../config";
import * as config from "config";
import * as log4js from "log4js";

log4js.configure(config.get("log4js"));
const log = log4js.getLogger();
const consoleLog = log4js.getLogger("console");

export function main(argv: string[]) {
    try {
        consoleLog.debug(`argv: ${argv}`);
        const message: string = argv[2];
        return `Hello ${message.charAt(0)}`;
    } catch (e) {
        log.error(e);
    } finally {
        log4js.shutdown((e) => e && console.log(e));
    }
}

if (require.main === module) {
    main(process.argv);
}

リリース

リリース用に、rm -rf相当のrimrafとcp -R相当のことができるcpxをインストールします。

npm i -DE rimraf cpx

package.jsonにはリリース用のスクリプト(release)と、パッケージ内に含めるファイル(files)と、node_modules/.binに生成される実行スクリプトの設定(ファイル名:sample、起動ソース:bin/main.js)を記載します。

(抜粋)package.json
  "scripts": {
    "test": "nyc -i ts-node/register --temp-directory coverage/.nyc -r text -r html -n test/**/*.ts -n src/**/*.ts -e .ts mocha test/**/*.ts",
    "release": "rimraf build bin && tsc && cpx build/src/** bin && cd build && npm pack -cwd .."
  },
  "files": [
    "bin",
    "config/default.json"
  ],
  "bin": {
    "sample": "bin/main.js"
  },

npm run releaseを実行すると、スクリプトが走ってbuild内にts-sample-1.0.0.tgzが出力されます。
このファイルを別のところに持っていって、npm i ts-sample-1.0.0.tgzとするとインストールができます。

これでプロジェクトの作成からリリースまで、ひととおりの作業ができるかと思います。

(その他)package.jsonの最終形

package.json
{
  "name": "ts-sample",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "test": "nyc -i ts-node/register --temp-directory coverage/.nyc -r text -r html -n test/**/*.ts -n src/**/*.ts -e .ts mocha test/**/*.ts",
    "release": "rimraf build bin && tsc && cpx build/src/** bin && cd build && npm pack -cwd .."
  },
  "files": [
    "bin",
    "config/default.json"
  ],
  "bin": {
    "sample": "bin/main.js"
  },
  "devDependencies": {
    "@types/config": "0.0.34",
    "@types/mocha": "2.2.48",
    "@types/node": "9.4.7",
    "cpx": "1.5.0",
    "espower-typescript": "8.1.3",
    "mocha": "5.0.4",
    "nyc": "11.6.0",
    "power-assert": "1.4.4",
    "rimraf": "2.6.2",
    "ts-node": "5.0.1",
    "tslint": "5.9.1",
    "typescript": "2.7.2"
  },
  "dependencies": {
    "config": "1.30.0",
    "log4js": "2.5.3"
  },
  "bundledDependencies": [
    "config",
    "log4js"
  ]
}