TypeScript Compiler API でビルド前にゴニョゴニョする話

# TypescriptCompilerAPI

## 導入
Typescriptを書いている際、ふと
「Typescriptのリフレクション事情ってどうなってるんだろう？」
と思~~ってしま~~い、興味本位で色々試してみることに。

## やりたいこと
+ 特定の型から派生した型を取得する
+ 型の詳細を取得する
+ publicなpropertyだけを出力

## 環境
```package.json
...
"devDependencies":{
    "linq": "^3.2.0",//(必須じゃない）
    "ts-node": "^8.3.0",
    "typescript": "^3.2.2"
}
```

## 型の取得
まずコンパイラそのもの(typescript)と、
.tsファイルをそのままnpm run 出来るようになるモジュール(tsnode)を導入する
~~あと趣味でLinqを導入する~~

`npm i --save-dev typescript ts-node linq`
適当なtypescriptファイルを作り、準備を整える

```a.ts
import * as TypeScript from 'typescript'

let scriptAction = () => {

}

scriptAction()

```

準備が出来たら**コード上で**コンパイルを行ってみる

``` ts
const program: TypeScript.Program = TypeScript.createProgram(
{
  options: {}, //?
  rootNames: entryPoints,
})

```
他にも入力できる値はあるが、必須なのはこの2つのようだ。

rootNameは`ReadonlyArray<string>`で**ファイル**もしくは**ディレクトリ**を指定でする。
ディレクトリなら中身を全てコンパイルしてくれる。

optionsは`CompilerOptions`型だが、手で入力するのは躊躇う物量だったため、
.tsconfigを引っ張れる手段が無いか調べてみる
最終的に`ParsedCommandLine.options`を拾えればうまく行きそうな雰囲気を感じたので引っ張ってみる

``` .ts
ParseTSConfig(rootDirectory: string, configName: string = 'tsconfig.json'): TypeScript.ParsedCommandLine {
    const parseConfigHost: TypeScript.ParseConfigHost = {
      fileExists: TypeScript.sys.fileExists,
      readFile: TypeScript.sys.readFile,
      readDirectory: TypeScript.sys.readDirectory,
      useCaseSensitiveFileNames: true,
    }

    const configFileName = TypeScript.findConfigFile(rootDirectory, TypeScript.sys.fileExists, configName)
    const configFile = TypeScript.readConfigFile(configFileName, TypeScript.sys.readFile)
    const compilerOptions = TypeScript.parseJsonConfigFileContent(configFile.config, parseConfigHost, rootDirectory)

    return compilerOptions
  }
```

こんな感じに

無事にTypescript.Programが生成出来たらいよいよソースファイルに手を付けていく
まずはソースファイルを引っ張るところから

```ts
  private QueryTargetSource(): IEnumerable<TypeScript.Node> {
　　// .tsconfigが持っているexclude
    const exclude: string[] = this.commandline.raw.exclude
    const result = Enumerator
      .from(this.program.getSourceFiles())
      .where(m => Enumerator.from(exclude).all(path => m.fileName.indexOf(path) === -1)
    )
    return result
  }
```

.tsconfigが持っているinclude/excludeは考慮されていなかったので
コンパイル後に弾いている（他に指定方法があるか要調査）

今回は**指定した型から派生した型**を列挙していく（直下の型のみ対応）

```ts
  public QuerrySubClasses(typeName: string) {
    const sourceFiles = this.QueryTargetSource()
    const typeNodes = sourceFiles
　　　 // 木を配列に（source => Types[])
      .select(m => this.FlatNode(m))
　　　 // 全ソースの物を纏める（sources[]）
      .selectMany(m => m)
　　　 // Classのみ抽出
      .where(m => m.kind === TypeScript.SyntaxKind.ClassDeclaration)
      .select(m => m as TypeScript.ClassLikeDeclarationBase)
　　　 // 基底クラスがある
      .where(m => m.heritageClauses !== undefined)
　　　 // 基底クラスのどれかが目的の型名と一致するか(2層以上遡るならここを再帰にする)
      .where(m =>
        Enumerator.from(m.heritageClauses)
          .selectMany(o => o.types)
          .any(n => n.getText() === typeName)
      )
      .toArray()
    return typeNodes
  }

 // hieralchyは継承ツリーなどを出力する際に使うと良い　不要なら削除してOK
 public FlatNode(node: TypeScript.Node, hieralchy: number = 0): IEnumerable<TypeScript.Node> {
    let nodes: TypeScript.Node[] = []
    nodes.push(node)
    node.forEachChild(m => {
      const childs = this.FlatNode(m, hieralchy + 1).toArray()
      nodes = nodes.concat(childs)
    })
    return Enumerator.from(nodes)
  }

```

上で列挙した型からpublicなpropertyだけを引っ張る

```ts

  GetAllMember(typeNode: TypeScript.ClassLikeDeclarationBase) {
    return Enumerator.from(typeNode.members)
      .where(m => this.IsPublicProperty(m))
      .toArray()
  }
  IsPublicProperty(element: TypeScript.ClassElement) {
    if (element.kind !== TypeScript.SyntaxKind.PropertyDeclaration) return false

    const property = element as TypeScript.PropertyDeclaration

    // no keyword
    if (!element.modifiers || element.modifiers.length === 0) return true

    if (element.modifiers.some(m => m.kind === TypeScript.SyntaxKind.StaticKeyword)) return false
    if (element.modifiers.some(m => m.kind === TypeScript.SyntaxKind.PrivateKeyword)) return false
    if (element.modifiers.some(m => m.kind === TypeScript.SyntaxKind.ProtectedKeyword)) return false
    if (element.modifiers.some(m => m.kind === TypeScript.SyntaxKind.AbstractKeyword)) return false
    if (element.modifiers.some(m => m.kind === TypeScript.SyntaxKind.ReadonlyKeyword)) return false

    return true
  }
```



## 動かす

まず動作させるためのスクリプトを作る

```ts
interface IClassValuesPair {
  classNode: TypeScript.ClassLikeDeclarationBase
  values: TypeScript.ClassElement[]
}

ToOutput(pair: IClassValuesPair): string {
  return `[${pair.classNode.name.getText()}]${pair.values
    .map(m => `\n  ${m.name.getText()}`)
    .reduce((m, n) => m + n, '')}`
}
class A {
  public static Generate(): void {
    const commandline = ParseTSConfig(process.cwd())
    const program = TypeScript.createProgram({
      options: commandline.options,
      rootNames: ["src/Main.ts"],
    })
    // IPluginOption　から派生した型を列挙する
    QuerrySubClasses("IPluginOption").map(m => {
      const pair: IClassValuesPair = {
        classNode: m,
        values: GetAllMember(m),
      }
      const result = ToOutput(pair)
      console.log(result)
    })
  }
}

A.Generate()
```

先ほど作った.tsファイルをプロジェクト内に配置し、package.jsonに次のような記述を追加する

```json
{
    "scripts": {
        "outputPublicProperty":"npx ts-node src/scripts/ファイル名.ts",
    }
}
```

これで.tsファイルをnpm run で呼び出せるようになる
`npm run outputPublicProperty`

```
[MarkdownOption]
  enable
  version
  activeCss
  useCodeBlockOverride
  languageToCssMap
[EmotionOption]
  enable
  version
```

出力はこんな感じ

これでコンパイラを利用した情報を程よく引っ張れるようになった
今回はプロジェクトでWebpackを使用していたので、上記のコードをWebpackのプラグインとして動作させることで
ビルドの際に勝手に動作するようにしている。

次回はWebpack.Configについて書く予定