Nuxt.js + TypeScript 環境に tslint を導入

## はじめに
単一ファイルコンポーネント（.vue）に tslint を導入しようと試みたところ
\<script lang="ts"\>\</script\> の部分以外にも lint が適応されてしまう問題に当たってしまった。
この記事ではその解決方法を交え tslint の導入方法を記載します。typescript の導入までは他の記事を参考にしたので、そもそも typescript の導入方法が異なる方は多少わかりづらいかも知れません。

## 前提
* 既に単一ファイルコンポーネントに typescript で書ける環境が整っていること。
* typescript の導入までは下記記事を参考にしましたので、ディレクトリー構造は下記記事と同様のまま説明していきます。  

　　※ 参考記事（typescriptの導入）
　　[create-nuxt-appで構築したNuxt2のプロジェクトにTypeScriptを入れてvuexを使えるところまで持っていく](https://qiita.com/TsukasaGR/items/5c911a59875dce87fe42)

## 使用するパッケージ
一応バージョンも記載しておきます

* tslint (v5.11.0）
* tslint-loader (v3.5.4)
* loader-utils (1.1.0)
* vue-parser (1.1.6)

## 手順

### STEP1： 必要なパッケージのインストール

``` bash:npmの場合
$ npm install --save-dev tslint tslint-loader loader-utils vue-parser
```
``` bash:yarnの場合
$ yarn add --dev tslint tslint-loader loader-utils vue-parser
```

### STEP2： tslint-loaderの修正（vue-parserを使用する）
まず tslint を導入するに当たって tslint-loader を webpack の設定へ噛ませる必要があるのですが、
tslint-loader をそのまま使用すると \<script lang="ts"\>\</script\> 以外の箇所も lint が適応されてしまうので
tslint-loader 自体を少し修正します。  

tslint-loader 自体は node_modules/ の中にあるので、
一旦コピーしたファイル（_tslint-loader.js）を modules/ ディレクトリーへ置きます。

``` bash:tslint-loaderをコピー
$ cp node_modules/tslint-loader/index.js modules/_tslint-loader.js
```
node_modules/tslint-loader/formatters/ の customFormatter.js も modules/ にコピーしておく
（無いと後々エラーになるため）

``` bash:formatterをコピー
$ cp node_modules/tslint-loader/formatters/customFormatter.js modules/formatters/customFormatter.js
```

コピーした _tslint-loader.js の下記コード部分に少し修正を加えます

``` js:modules/_tslint-loader.js（修正前）
module.exports = function(input, map) {
  this.cacheable && this.cacheable();
  var callback = this.async();

  if (!semver.satisfies(Lint.Linter.VERSION, '>=4.0.0')) {
    throw new Error('Tslint should be of version 4+');
  }

  var options = resolveOptions(this);
  lint(this, input, options);
  callback(null, input, map);
};
```

``` js:modules/_tslint-loader.js（修正後）
var vueParser = require('vue-parser');
//...
module.exports = function(input, map) {
  this.cacheable && this.cacheable();
  var callback = this.async();

  if (!semver.satisfies(Lint.Linter.VERSION, '>=4.0.0')) {
    throw new Error('Tslint should be of version 4+');
  }

  var options = resolveOptions(this);
  // https://github.com/wbuchwalter/tslint-loader/issues/105
  if (/<script.*lang="ts".*>([\s|\S]*)<\/script>/.test(input)) {
    const _input = vueParser.parse(input, 'script', { lang: ['ts', 'tsx'] });
    lint(this, _input, options);
  } else {
    lint(this, input, options);
  }
  callback(null, input, map);
};
```

### STEP3： tslint-loader を webpack へ噛ませる
修正した _tslint-loader.js を webpack へ噛ませるために modules/typescript.js を修正します。

``` js:modules/typescript.js（修正前）
module.exports = function() {
  // Add .ts extension for store, middleware and more
  this.nuxt.options.extensions.push("ts")
  // Extend build
  this.extendBuild(config => {
    const tsLoader = {
      loader: "ts-loader",
      options: {
        appendTsSuffixTo: [/\.vue$/]
      }
    }
    // Add TypeScript loader
    config.module.rules.push(
      Object.assign(
        {
          test: /((client|server)\.js)|(\.tsx?)$/
        },
        tsLoader,
      )
    )
    // Add TypeScript loader for vue files
    for (let rule of config.module.rules) {
      if (rule.loader === "vue-loader") {
        rule.options.loaders = {
          ts: "tsLoader"
        }
      }
    }
    // Add .ts extension in webpack resolve
    if (config.resolve.extensions.indexOf(".ts") === -1) {
      config.resolve.extensions.push(".ts")
    }
  })
}
```

``` js:modules/typescript.js（修正後）
const path = require('path');

module.exports = function() {
  // Add .ts extension for store, middleware and more
  this.nuxt.options.extensions.push("ts")
  // Extend build
  this.extendBuild(config => {
    const tsLoader = {
      loader: "ts-loader",
      options: {
        appendTsSuffixTo: [/\.vue$/]
      }
    }
    const tslintLoader = {
      loader: path.resolve(__dirname, '_tslint-loader.js'),
      // tslint の linterOptions.exclude で除外ファイルを指定できますが、
      // コマンドラインから tslint を実行した場合のみ有効なので、下記で除外ファイルを指定する必要があリます
      exclude: /node_modules|\.nuxt/,
      enforce: 'pre',
      options: {
        configFile: 'tslint.json',
      }
    }
    // Add TypeScript loader
    config.module.rules.push(
      Object.assign(
        {
          test: /((client|server)\.js)|(\.tsx?)$/
        },
        tsLoader,
      )
    )
    // Add TypeScript loaders
    config.module.rules.push(
      Object.assign(
        {
          test: /((client|server)\.js)|(\.tsx?)$/
        },
        tslintLoader,
      )
    )
    // Add TypeScript loader for vue files
    for (let rule of config.module.rules) {
      if (rule.loader === "vue-loader") {
        rule.options.loaders = {
          ts: "tsLoader!tslintLoader"
        }
      }
    }
    // Add .ts extension in webpack resolve
    if (config.resolve.extensions.indexOf(".ts") === -1) {
      config.resolve.extensions.push(".ts")
    }
  })
}
```

### STEP4： tslint.json ファイルを作成する

``` bash:tslint.jsonファイルの作成
$ touch tslint.json
```

``` json:tslint.json（例）
{
  "defaultSeverity": "error",
  "extends": "tslint:recommended",
  "rules": {
    "no-namespace": false,
    "no-string-literal": false,
    "max-line-length": {
      "options": {"limit": 120},
      "severity": "warn"
    },
    "interface-name": [true,  "never-prefix"],
    "ordered-imports": false,
    "object-literal-sort-keys": false
  }
}
```

### STEP5： 完了
これで一通り完了なので、Nuxtの開発環境を立ち上げてみましょう。

``` bash:nuxtの開発環境立ち上げ
# yarn
$ yarn run dev
# npm
$ npm run dev
```
下記のように tslint が動作していれば完了です🎉
![image.png](https://qiita-image-store.s3.amazonaws.com/0/328866/dca98e99-aa93-967c-9118-8046b2adfabe.png)

## オプション
tslint は正常に動作するようになったものの、
少し見にくいのでログを見やすくするために formatter を追加します

### STEP1
custom-tslint-formatters をインストールします（npm での install は割愛）

``` bash
$ yarn add --dev custom-tslint-formatters
```

### STEP2
options に formatter と formattersDirectory を追加

``` js:modules/typescript.js
    const tslintLoader = {
      loader: path.resolve(__dirname, '_tslint-loader.js'),
      // tslint の linterOptions.exclude で除外ファイルを指定できますが、
      // コマンドラインから tslint を実行した場合のみ有効なので、下記で除外ファイルを指定する必要があリます
      exclude: /node_modules|\.nuxt/,
      enforce: 'pre',
      options: {
        configFile: 'tslint.json',
        // ↓ formatter と formattersDirectory オプションを追加
        formatter: 'grouped',
        formattersDirectory: 'node_modules/custom-tslint-formatters/formatters',
      }
    }
```

### STEP3

Nuxtの開発環境を立ち上げて確認する。
lint の error 内容が見やすくなりました🎉
![image.png](https://qiita-image-store.s3.amazonaws.com/0/328866/c3cd47b5-3302-e5cb-0035-16fd12dc00ab.png)

## 最後に
何か記事に間違いなどありましたらご指摘よろしくお願いします。
参考にしたURLをまとめておきます

* [create-nuxt-appで構築したNuxt2のプロジェクトにTypeScriptを入れて... Qiita](https://qiita.com/TsukasaGR/items/5c911a59875dce87fe42)
* [tslint の linterOptions.exclude に関する github_issue](https://github.com/Microsoft/vscode-tslint/issues/325#issuecomment-366202827)
* [vue-loader に loaders に tslint-loader を指定 github_issue](https://github.com/palantir/tslint/issues/2099#issuecomment-292524819)
* [単一ファイルコンポーネント（.vue）に対する tslint-loader で発生する問題 github_issue](https://github.com/wbuchwalter/tslint-loader/issues/105)