TypeScriptで書かれている.vueファイルの割合を算出する

# はじめに
みなさんご存知だとは思いますが、GitHubにはRepository内で使われているプログラミング言語の割合が表示されています。
おそらくですが、あれはファイルの拡張子でどのプログラミング言語なのかを判断しているのでしょう。
さて、JavaScriptのライブラリであるVue.jsでは1つのコンポーネントを拡張子が`.vue`であるファイルを作成、すなわちSingleFileComponentという仕組みで開発していくのが主流ですね。
この場合GitHubのLanguage欄は以下のようになります。

`.vue`ファイルなので当然Vueとして認識されてますね。
9割9分9厘の方は別にそれで何も気にしないと思いますが、`.vue`ファイルの`script`ブロックにはJavaScriptでもTypeScriptでも記述することができるので、`.vue`ファイルがJavaScriptなのか、TypeScriptなのか気になることがありませんか？
私は気になります。

というわけで、プロジェクトの`.vue`ファイル全体からどれくらいの割合がTypeScriptで記述されているのかを算出できる仕組みを作ってみましょう。

# SFCをパースする

まず、どのようにして`.vue`ファイルの中身がJavaScriptなのか、TypeScriptなのか判断するかを考えていきたいと思います。
TypeScriptで書かれた`.vue`ファイルの`script`ブロックには、`lang`ディレクティブに`ts`が指定されています。

```.vue
// JavaScriptの場合
<script>
</script>
// TypeScriptの場合
<script lang="ts">
</script>
```

であれば、`.vue`ファイルをパースして`script`ブロックの`lang`属性の値を取れば良いですね。
早速、`.vue`ファイルをパースする処理を書いて......いく必要はないです。

Vue.jsの中には`vue-template-compiler`というVue.jsのtemplateをコンパイルするためのAPIがパッケージとして存在しており、そこに`parseComponent`というSFCをパースするためのメソッドが用意されています。
今回はこれを使って実現しようと思います。
https://github.com/vuejs/vue/tree/dev/packages/vue-template-compiler#readme

```.js
const compiler = require('vue-template-compiler')

const sfc = `
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png" />
    <HelloWorld msg="Welcome to Your Vue.js App" />
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
`

const descriptor = compiler.parseComponent(sfc)

console.log(descriptor)
```

使い方としては、引数にSFCの内容を文字列として渡してあげれば、以下のようなオブジェクトを返してくれます。

```.js
{
  template: {
    type: 'template',
    content: '\n' +
      '<div id="app">\n' +
      '  <img alt="Vue logo" src="./assets/logo.png" />\n' +
      '  <HelloWorld msg="Welcome to Your Vue.js App" />\n' +
      '</div>\n',
    start: 11,
    attrs: {},
    end: 141
  },
  script: {
    type: 'script',
    content: '\n' +
      "import HelloWorld from './components/HelloWorld.vue'\n" +
      '\n' +
      'export default {\n' +
      "  name: 'App',\n" +
      '  components: {\n' +
      '    HelloWorld\n' +
      '  }\n' +
      '}\n',
    start: 162,
    attrs: {},
    end: 286
  },
  styles: [
    {
      type: 'style',
      content: '\n' +
        '#app {\n' +
        '  font-family: Avenir, Helvetica, Arial, sans-serif;\n' +
        '  -webkit-font-smoothing: antialiased;\n' +
        '  -moz-osx-font-smoothing: grayscale;\n' +
        '  text-align: center;\n' +
        '  color: #2c3e50;\n' +
        '  margin-top: 60px;\n' +
        '}\n',
      start: 304,
      attrs: {},
      end: 504
    }
  ],
  customBlocks: [],
  errors: []
}
```

`script`ブロックに、lang属性が含まれているSFCをパースすると以下のようにちゃんとlang属性の値が入っています。

```.js
{
  template: {
    type: 'template',
    content: '\n<div>Hello</div>\n',
    start: 10,
    attrs: {},
    end: 30
  },
  script: {
    type: 'script',
    content: '',
    start: 60,
    attrs: { lang: 'ts' },
    lang: 'ts',
    end: 60
  },
  styles: [ { type: 'style', content: '', start: 77, attrs: {}, end: 77 } ],
  customBlocks: [],
  errors: []
}
```

問題なさそうですね。

では、この仕組を作ってサクッと入力された.vueファイルからTypeScriptであるものの割合を出力するプログラムを書いていきます

```lib/index.js
const compiler = require('vue-template-compiler')
const fs = require('fs')

function filterVueFiles(files) {
  return files.filter((file) => file.match(/.vue$/))
}

function calcTypeScriptRate(files) {
  const descriptors = files.map((file) => {
    const component = fs.readFileSync(file).toString()
    return compiler.parseComponent(component)
  })
  const totalCount = files.length
  const tsCount = descriptors.filter(
    (desc) => desc.script?.attrs?.lang === 'ts'
  ).length
  return tsCount / totalCount
}

module.exports = {
  filterVueFiles,
  calcTypeScriptRate,
}
```

```cli.js
const { program } = require('commander')
const { filterVueFiles, calcTypeScriptRate } = require('./lib')

program.parse()

const options = program.opts()
const files = filterVueFiles(program.args)
const rate = calcTypeScriptRate(files)

console.log(rate)
```

こんな感じでしょうか。
componentsディレクトリに3つの`.vue`ファイル、そのうちの1つがTypeScriptで書かれている想定で実行すると以下のように出力されます。

```shell
$ node cli.js components/**
0.3333333333333333
```

このプログラムをもう少し整えたものは以下のリポジトリにおいてあります。
https://github.com/fuku710/vue-sfc-ts-rate

# おわりに
`.vue`ファイルのTypeScriptの割合を算出するという、おおよそほとんどの人間には需要がないものですが、Vue.js内部のAPIを用いいればこのようなものがサクッと作れると思います。
本当であればGitHubActionsを用いてpushするたびにリポジトリ内のTypeScript率を算出して、README.mdなどにbadgeとして表示したかったのですが、どうやってもGitHubだけで完結する手軽な方法が存在しないため、諦めました。
GitHubActionsでパーセンテージが表示できるbadgeが出力できたらなぁ...