TypescriptCompilerAPI
導入
Typescriptを書いている際、ふと
「Typescriptのリフレクション事情ってどうなってるんだろう?」
と思ってしまい、興味本位で色々試してみることに。
やりたいこと
- 特定の型から派生した型を取得する
- 型の詳細を取得する
- publicなpropertyだけを出力
環境
...
"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ファイルを作り、準備を整える
import * as TypeScript from 'typescript'
let scriptAction = () => {
}
scriptAction()
準備が出来たらコード上でコンパイルを行ってみる
const program: TypeScript.Program = TypeScript.createProgram(
{
options: {}, //?
rootNames: entryPoints,
})
他にも入力できる値はあるが、必須なのはこの2つのようだ。
rootNameはReadonlyArray<string>
でファイルもしくはディレクトリを指定でする。
ディレクトリなら中身を全てコンパイルしてくれる。
optionsはCompilerOptions
型だが、手で入力するのは躊躇う物量だったため、
.tsconfigを引っ張れる手段が無いか調べてみる
最終的にParsedCommandLine.options
を拾えればうまく行きそうな雰囲気を感じたので引っ張ってみる
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が生成出来たらいよいよソースファイルに手を付けていく
まずはソースファイルを引っ張るところから
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は考慮されていなかったので
コンパイル後に弾いている(他に指定方法があるか要調査)
今回は指定した型から派生した型を列挙していく(直下の型のみ対応)
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だけを引っ張る
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
}
動かす
まず動作させるためのスクリプトを作る
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に次のような記述を追加する
{
"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について書く予定