Typescript Compiler APIを斜め読み

TypeScript Compiler APIを利用して独自のAST変換機構を試すために調べたメモ。

TypescriptCompiler APIを利用することでASTの解析および変更が可能。
独自の型解析機構や構文生成機構を作成できる。例えば、TSからJSへの変更の際に独自に必要な負荷情報を追加・編集してコンパイルすることが可能になる。
ちなみにASTの情報はAST Exploerで確認ができるため、開発の際にはこれを参考にする。

簡単なtransformコンパイラを作成して見ていく。

Config parser

まずはtypescriptのconfig解析機構。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import * as ts from 'typescript';

const configFileName: string[] = process.argv.slice(2);
// compiler option
const optionToExpend: ts.CompilerOptions = null;
// config file information
const createConfigFileHost: ts.ParseConfigFileHost = {
// Reporter of unrecoverable error
// When parsing config file
onUnRecoverableConfigFileDiagnostic(diagnostic: ts.Diagnostic) {
console.error(diagnostic);
throw new Error(ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"));
},
useCaseSensitiveFileNames: false, // file names are treated with case sensitivity <boolean>
readDirectory: ts.sys.readDirectory, // used to cache and handle directory structure modifications <string>
fileExists: ts.sys.fileExists, // specified path exists and is a file. <boolean>
readFile: ts.sys.readFile, // Use to read file text for source files <string>
getCurrentDirectory: ts.sys.getCurrentDirectory, // current directory of the program <strig>
};
const configParseResult: ts.ParsedCommandLine = ts.getParsedCommandLineOfConfigFile(configFileName[0], optionToExpend, createConfigFileHost);
console.log(configParseResult);

getParsedCommandLineOfConfigFileを使うことでファイル名を指定してconfigファイルをreadできる。
解析結果のオブジェクトをもとにプログラムを作成し、コンパイルを行う

コンパイル関数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// typescript compile option
const CJS_CONFIG: ts.CompilerOptions = {
experimentalDecorators: true,
jsx: ts.JsxEmit.React,
module: ts.ModuleKind.ESNext,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
noEmitOnError: false,
noUnusedLocals: true,
noUnusedParameters: true,
stripInternal: true,
resolveJsonModule: true,
declaration: true,
baseUrl: __dirname,
target: ts.ScriptTarget.ES5
};

function compile(
input: string,
options: ts.CompilerOptions = CJS_CONFIG
) {
const file = globSync(input);
const compilerHost = ts.createCompilerHost(options);
const program: ts.Program = ts.createProgram(file, options, compilerHost);

const sourceFile: ts.SourceFile = program.getSourceFile(input);
const WriteFileCallback = (fileName, data, writeByteOrderMark, onError, sourceFiles) => {
fs.writeFileSync(fileName, data, 'utf8');
}
const cancellationToken: ts.CancellationToken = undefined;
const emitOnlyDtsFiles: boolean = false;
const customTransformers: ts.CustomTransformers = {
before: [], // to evaluate before built-in .js transformations.
after: [], // to evaluate after built-in .js transformations.
afterDeclarations: [], // to evaluate after built-in .d.ts transformations
};
// EmitResult {
// emitSkipped: boolean;
// diagnostics: ReadonlyArray<Diagnostic>;
// emittedFiles?: string[];
// }
const emitResult: ts.EmitResult = program.emit(sourceFile, WriteFileCallback, cancellationToken, emitOnlyDtsFiles, customTransformers);
const allDiagnostics = ts
.getPreEmitDiagnostics(program)
.concat(emitResult.diagnostics);

allDiagnostics.forEach(diagnostic => {
let { line, character } = diagnostic.file.getLineAndCharacterOfPosition(
diagnostic.start
);
let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
console.log(
`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`
);
});

const CarriageReturnLineFeed: ts.NewLineKind = 0;
const printerOptions: ts.PrinterOptions = {
removeComments: true,
newLine: CarriageReturnLineFeed,
omitTrailingSemicolon: false,
noEmitHelpers: true,
}

// debug code print
// const printer: ts.Printer = ts.createPrinter(printerOptions)
// const code: string = printer.printFile(sourceFile);
// console.log('code: =>', code)

const exitCode = emitResult.emitSkipped ? 1 : 0;
if (exitCode) {
console.log(`Process exiting with code '${exitCode}'.`);
}

process.exit(exitCode);
}

createProgramでProgramというコンパイル単位のインスタンスを生成する。
Programインスタンスはコンパイルに必要な設定情報やソースファイル情報などをもっている。

Programのemit()によりjsファイル、宣言ファイルを発行する。
ここでtargetSourceFileがない場合は、すべての対ファイルに対してのjsファイルと宣言ファイルが発行される。
emmit()のcustomTransformersオプションには、独自のtransform関数をフックできるようになっている。
それぞれ、jsファイル発行前後、宣言ファイル発行後のイベントフックできる。

各ステップでのコンパイルの結果はDiagnosticという共通のInterfaceをもっており、
programインスタンスから収集が可能になっている。

コンパイル結果のjsコード結果を取得する場合はcreatePrinter()を使い、printerインスタンスを使う。

今度はコードのtransformを行うcustom transform関数をcustomTransformersに追加する。
custom transformにはtypescriptのASTが与えられる。
またvisitorというAST解析、コード修正を再帰的に呼び出す必要がある。

簡単なtransformerを作成する。

1
2
3
4
5
6
7
function customTransform(options: customTransformOption): ts.TransformerFactory<ts.SourceFile> {
return (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
return (sorceFile: ts.SourceFile) => {
return ts.visitNode(sorceFile, transformer(context, sorceFile, options))
}
}
}

transformerはNode変更に必要な関数TransformerFactoryを生成する必要がある。
visitNodeで所定のNodeに対してtransnformを適用する(ここではtransformer)。
transformerの処理は以下のようにする。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function transformer(context: ts.TransformationContext, sorce: ts.SourceFile, options: customTransformOption): ts.Visitor {
const visitor: ts.Visitor = (node: ts.Node): ts.Node[]|ts.Node => {
if (ts.isFunctionDeclaration(node)) {
const declaration: ts.VariableDeclaration = ts.createVariableDeclaration(
ts.createIdentifier('val'),
ts.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword),
ts.createNumericLiteral('1000')
);
const declarations: ts.VariableDeclaration[] = [declaration];
return [ts.createVariableDeclarationList(declarations, ts.NodeFlags.Const), node];
}
return ts.visitEachChild(node, visitor, context);
}
return visitor;
}

今回は関数宣言を変数宣言に置き換える簡単なものを作成する。
createVariableDeclarationListを作成して新たなNodeを生成しているが、
updateVariableDeclarationListなどもあり、既存のNodeに追加することも可能である。
基本的にcreateupdateはそれぞれサポートされているのでオリジナルのNodeを更新することも可能。
webpackやrollupなどのツールの場合、独自のtransformerの設定がサポートされているので、自前のビルド環境に組み込むことは簡単。

1
2
3
4
5
6
7
8
9
10
// webpack.config.js
{
test: /\.ts$/,
loader: ‘ts-loader’,
options: {
getCustomTransformers: () => ({
after: [customTransform] // set custom transformer
}),
transpileOnly: true
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// rollup.config.js
import typescript from 'rollup-plugin-typescript2';
import customTransform from 'path/to/customTransform';

export default {
// ...
plugins: [
typescript({ transformers: [service => ({
before: [],
after: [ customTransform() ]
})] })
]
};

このtransformerを実際に実行するためにいくつか変更をする。

1
2
3
4
- const WriteFileCallback = (fileName, data, writeByteOrderMark, onError, sourceFiles) => {}
+ const WriteFileCallback = (fileName, data, writeByteOrderMark, onError, sourceFiles) => {
+ fs.writeFileSync(fileName, data, 'utf8');
+ }
1
2
3
4
5
6
7
8
9
10
11
12
const customTransformers: ts.CustomTransformers = {
before: [], // to evaluate before built-in .js transformations.
after: [], // to evaluate after built-in .js transformations.
- afterDeclarations: [], // to evaluate after built-in .d.ts transformations
};
const customTransformers: ts.CustomTransformers = {
+ before: [
+ customTransform({program})
+ ], // to evaluate before built-in .js transformations.
after: [], // to evaluate after built-in .js transformations.
afterDeclarations: [], // to evaluate after built-in .d.ts transformations
};
1
+ compile(file, CJS_CONFIG);

この変更を加えて次のファイルのcompileを実行する。

1
2
// example.ts
function Example1() {};
1
$ npx ts-node custom-ast-transform.ts example.ts

transformされた結果としてのコードを得ることができる。

1
2
3
4
// example.js
var val = 1000
function Example1() { }
;

コールスタックをみていくと、program#emitでprogram#runWithCancellationTokenが呼ばれ、このrunWithCancellationTokenは処理をprogram#emitWorkerに移譲する。
この内部でcreateTypeCheckerによりcheckerオブジェクトが渡される。これにより多くのAPIが提供される。その一つのTypeChecker#getEmitResolverによりemitresolverが提供される。

1
2
3
4
5
program#emit
program#runWithCancellationToken
program#emitWorker
checker#createTypeChecker
=> checker

引き続きemitWorker内でemitFilesが呼ばれ、オプションとしてsourceFileとemitResolverが渡される。

emitFilesではtsをjsにtransformしていく。
forEachEmittedFileのオプションで渡されるemitSourceFileOrBundleのemitJsFileOrBundleでtransformNodesが呼ばれ変換される。
transformationで変換する。

1
2
3
4
5
6
emitter#emitFiles
emitter#forEachEmittedFile
emitSourceFileOrBundle
emitJsFileOrBundle
transformer#transformNodes
transformer#transformation

最後にprintSourceFileOrBundleが実施されてファイルに出力される。

今回は調査のために簡単なサンプルを作成しただけになるが、Typescript Compiler APIを活用することで独自の型チェック機構(linter)のようなものや型解析からのドキュメント自動生成、Debuger、polyfillの自動追加機構などのプラグインを作成可能になる。

APIの変更履歴はAPI Breaking Changesにあるので、利用するTypescriptのバージョンによっては確認しておくと良さそう。

型推論自体はinferTypesで行われ、このメソッドはcreateTypeCheckerで生成されるcheckerを経由して実行されている。
initializeTypeChecker

1
2
3
program#getTypeChecker
ts#createTypeChecker
initializeTypeChecker

型推論に関しては別途で見てみよう。

参考ページ

Using the Compiler API

Comments