banner
Madinah

Madinah

github
twitter
telegram
email
bilibili

TSコンパイルAPI

背景#

以前、SDK のコンパイルプロセスを行う際、開発者が開発しやすいように、長い相対パスを短いエイリアスに変換するためにいくつかのエイリアスを作成することがよくありました。
例えば

{
  // ...
  "baseUrl":"src",
  "paths": {
    "@/package": ["./index"],
    "@/package/*": ["./*"],
  }
}

このようにすることで、エイリアスは常に src ディレクトリを指すことになり、src ディレクトリ内の非常に深い階層のファイルが最上層のファイルを参照する際に、直接書くことができ、相対パスの参照を減らし、コードを見栄え良くし、ファイルのディレクトリ構造を調整しやすくなります。

import Components from "@/package/ui/header"

このように、開発段階では DX 体験が良好ですが、最終的にコードを SDK にパッケージ化する際、SDK を使用するホスト環境が私たちが設定したのと同じエイリアスであるとは限らず、一致しない場合はファイルが見つからない問題が発生します。これにより、TS をパッケージ化する際にエイリアスに関連するファイルを相対パスにコンパイルし、すべての参照者の互換性を実現する必要があります。これには TS のコンパイルプロセスについての理解が必要です。

TS compiler 相关#

TS のコンパイルプロセス#

SourceCode(ソースコード) ~~ スキャナー(Scanner) ~~> トークン流
トークン流 ~~ パーサー (parser) ~~> AST(抽象構文木)
AST ~~ バインダー (binder) ~~> シンボル(Symbols)
AST + シンボル(Symbols) ~~ チェッカー ~~> 型検証
AST + チェッカー ~~ エミッター(emitter) ~~> JavaScript コード

スキャナー#

TS(TypeScript)におけるスキャナーはコンパイラの最初の段階であり、字句解析器とも呼ばれます。これは、ソースコードファイル内の文字列を一連の字句単位(tokens)に変換する役割を担っています。
動作は以下の通りです:

  1. 文字列の読み込み:スキャナーはソースコードファイルから文字を一つずつ読み取ります。
  2. 字句単位の認識:スキャナーは一連の事前定義された文法ルールに基づいて、文字を組み合わせて認識された字句単位を生成します。これには識別子、キーワード、演算子、定数などが含まれます。有限オートマトン(finite automation)や正規表現を使用して文字列をマッチさせます。
  3. 字句単位の生成:完全な字句単位が認識されると、スキャナーはそれを型と値の情報を含むオブジェクトとして生成し、コンパイラの次の段階に渡します。
  4. 特殊なケースの処理:スキャナーは同時にコメント、文字列リテラル、エスケープ文字の解析などの特殊なケースも処理します。

例えば、以下の TypeScript コードスニペットを考えてみましょう:

let age: number = 25;

スキャナーは文字を一つずつ読み取り、以下の字句単位を生成します:

  1. let キーワード
  2. age 識別子
  3. : コロン(演算子)
  4. number キーワード
  5. = 等号(演算子)
  6. 25 数字定数
  7. ; セミコロン(区切り記号)

字句単位生成の順序は文法ルールによって定義されており、スキャナーはソースコードファイル内のすべての文字が処理されるまでこのプロセスを繰り返します。この段階では関連するトークンを抽出するだけで、文法や意味に関連する分析は行われません。

import * as ts from "typescript";

// TypeScriptにはシングルトンスキャナーがあります
const scanner = ts.createScanner(ts.ScriptTarget.Latest, /*skipTrivia*/ true);

// これは`initializeState`という関数を使用して初期化されます
function initializeState(text: string) {
  scanner.setText(text);
  scanner.setOnError((message: ts.DiagnosticMessage, length: number) => {
    console.error(message);
  });
  scanner.setScriptTarget(ts.ScriptTarget.ES5);
  scanner.setLanguageVariant(ts.LanguageVariant.Standard);
}

// サンプル使用法
initializeState(`
var foo = 123;
`.trim());

// スキャンを開始
var token = scanner.scan();
while (token != ts.SyntaxKind.EndOfFileToken) {
  console.log(ts.SyntaxKind[token]);
  token = scanner.scan();
}

出力

VarKeyword
Identifier
FirstAssignment
FirstLiteralToken
SemicolonToken

パーサー#

TS(TypeScript)のパーサーは TypeScript コードを抽象構文木(Abstract Syntax Tree、略して AST)に変換するためのツールです。パーサーの主な役割は、ソースコードを構文木に解析し、後続の静的分析、型チェック、コンパイルなどの操作を行うことです。
パーサーはソースコードの字句(Lexical)および構文(Syntactic)構造を分析して構文木を構築します。字句解析段階ではソースコードがトークン(Tokens)に分解され、例えばキーワード、識別子、演算子、定数などが含まれます。構文解析段階ではトークンがツリー構造に整理され、コードの構文の正しさが保証されます。

import * as ts from "typescript";

function printAllChildren(node: ts.Node, depth = 0) {
  console.log(new Array(depth + 1).join('----'), ts.SyntaxKind[node.kind], node.pos, node.end);
  depth++;
  node.getChildren().forEach(c=> printAllChildren(c, depth));
}

var sourceCode = `
var foo = 123;
`.trim();

var sourceFile = ts.createSourceFile('foo.ts', sourceCode, ts.ScriptTarget.ES5, true);
printAllChildren(sourceFile);

出力

SourceFile 0 14
---- SyntaxList 0 14
-------- VariableStatement 0 14
------------ VariableDeclarationList 0 13
---------------- VarKeyword 0 3
---------------- SyntaxList 3 13
-------------------- VariableDeclaration 3 13
------------------------ Identifier 3 7
------------------------ FirstAssignment 7 9
------------------------ FirstLiteralToken 9 13
------------ SemicolonToken 13 14
---- EndOfFileToken 14 14

バインダー#

一般的な JavaScript パーサーのプロセスは大体次のようになります

SourceCode ~~スキャナー~~> トークン ~~パーサー~~> AST ~~エミッター~~> JavaScript

しかし、上記のプロセスは TS にとって重要なステップが欠けています。TypeScript の意味** システムです。** これは、チェック器が型チェックを実行するのを助けるために、バインダーがソースコードの各部分を関連する型システムに接続し、チェック器が使用できるようにします。バインダーの主な責任は_シンボル_(Symbols)を作成することです。

  1. 簡単な理解

image

  1. 深く構造を探る

image

image.png#

内部の pos end を使用してスコープ関連の参照の一意性を判断できます。

チェッカー#

ここでは、上記のバインダーから生成されたシンボルと共に型推論、型チェックなどを行います。
コード例

import * as ts from "typescript";
import path from 'path'

// TypeScriptプロジェクトを作成
const program = ts.createProgram({
  rootNames: [path.join(__dirname, './check.ts')], // プロジェクト内のすべてのチェック対象ファイルのパス
  options: {
    ...ts.getDefaultCompilerOptions(),
    baseUrl: '.'
  }, // コンパイルオプション
});

// プロジェクト内のすべての意味的エラーを取得
const diagnostics = ts.getPreEmitDiagnostics(program)

// エラーメッセージを印刷
diagnostics.forEach((diagnostic) => {
  console.log(
    `Error: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`
  );
});

check.ts

const a:string  = 1
console.log(a)

const b = ({)

出力

Error: Type 'number' is not assignable to type 'string'.
Error: Property assignment expected.
Error: '}' expected.

エミッター#

  • emitter.ts:TS -> JavaScript のエミッター
  • declarationEmitter.ts:このエミッターは_TypeScript ソースファイル(.ts)_のための_宣言ファイル_を作成します。

Emit 段階では Printer が AST をテキストに変換します。Printer(プリンター)という名前は非常に適切で、AST を印刷してテキストにします。

import * as ts from 'typescript';

const printer = ts.createPrinter();
const result = printer.printNode(
  ts.EmitHint.Unspecified,
  makeNode(),
  undefined,
);
console.log(result);

function makeNode() {
  return ts.factory.createVariableStatement(
    undefined,
    ts.factory.createVariableDeclarationList(
      [
        ts.factory.createVariableDeclaration(
          ts.factory.createIdentifier('video'),
          undefined,
          ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword),
          ts.factory.createStringLiteral('conference'),
        ),
      ],
      ts.NodeFlags.Const,
    ),
  );
}

トランスフォーマー#

上記の部分では、TS コンパイルコードのいくつかのプロセスを紹介しましたが、TS はまた「ライフサイクルフック」のようなものを提供し、コンパイルプロセスの中で自分自身のカスタム部分を追加することを許可します。

  • before TypeScript の前にトランスフォーマーを実行します(コードはまだコンパイルされていません)
  • after TypeScript の後にトランスフォーマーを実行します(コードはコンパイル済み)
  • afterDeclarations 宣言ステップの後にトランスフォーマーを実行します(ここで型定義を変換できます)

API#

visiting#

  • ts.visitNode (node, visitor) はルートノードを遍歴するために使用されます
  • ts.visitEachChild (node, visitor, context) は子ノードを遍歴するために使用されます
  • ts.isXyz (node) はノードのタイプを判断するために使用されます。例えば ts.isVariableDeclaration (node)

ノード#

  • ts.createXyz 新しいノードを作成します(そして返します)、ts.createIdentifier ('world')
  • ts.updateXyz ノードを更新するために使用されます ts.updateVariableDeclaration ()

トランスフォーマーを書く#

const transformer =
  (_program: ts.Program) => (context: ts.TransformationContext) => {
    return (sourceFile: ts.Bundle | ts.SourceFile) => {
      const visitor = (node: ts.Node) => {
        console.log('zxzxxxx', node);
        if (ts.isIdentifier(node)) {
          switch (node.escapedText) {
            case 'babel':
              return ts.factory.createStringLiteral('babel-transformer');
            case 'typescript':
              return ts.factory.createStringLiteral('typescript-transformer');
          }
        }
        return ts.visitEachChild(node, visitor, context);
      };

      return ts.visitNode(sourceFile, visitor);
    };
  };

const program = ts.createProgram([path.join(__dirname, './02.ts')], {
  baseUrl: '.',
  target: ts.ScriptTarget.ESNext,
  module: ts.ModuleKind.ESNext,
  declaration: true,
  declarationMap: true,
  jsx: ts.JsxEmit.React,
  moduleResolution: ts.ModuleResolutionKind.NodeJs,
  skipLibCheck: true,
  allowSyntheticDefaultImports: true,
  outDir: path.join(__dirname, '../dist/transform'),
});

const res = program.emit(undefined, undefined, undefined, undefined, {
  after: [transformer(program)],
});
console.log(res);

image

さらなるコード例 https://github.com/itsdouges/typescript-transformer-handbook/tree/master/example-transformers

実際の具体的な応用#

import path from 'path';
import { chain, head, isEmpty } from 'lodash';
import ts from 'typescript';

export function replaceAlias(
  fileName: string,
  importPath: string,
  paths?: Record<string, string[]>
) {
  if (isEmpty(paths)) return importPath;

  const normalizedPaths = chain(paths)
    .mapKeys((_, key) => key.replace(/\*$/, ''))
    .mapValues(head)
    .omitBy(isEmpty)
    .mapValues((resolve) => (resolve as string).replace(/\*$/, ''))
    .value();

  for (const [alias, resolveTo] of Object.entries(normalizedPaths)) {
    if (importPath.startsWith(alias)) {
      const resolvedPath = importPath.replace(alias, resolveTo);
      const relativePath = path.relative(path.dirname(fileName), resolvedPath);
      return relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
    }
  }

  return importPath;
}

export default function (_program?: ts.Program | null, _pluginOptions = {}) {
  return ((ctx) => {
    const { factory } = ctx;
    const compilerOptions = ctx.getCompilerOptions();

    return (sourceFile: ts.Bundle | ts.SourceFile) => {
      const { fileName } = sourceFile.getSourceFile();
      function traverseVisitor(node: ts.Node): ts.Node | null {
        let importValue: string | null = null;
        if (ts.isCallExpression(node)) {
          const { expression } = node;
          if (node.arguments.length === 0) return null;
          const arg = node.arguments[0];
          if (!ts.isStringLiteral(arg)) return null;
          if (
            // Can't call getText on after step
            expression.getText(sourceFile as ts.SourceFile) !== 'require' &&
            expression.kind !== ts.SyntaxKind.ImportKeyword
          )
            return null;
          importValue = arg.text;
          // import, export
        } else if (
          ts.isImportDeclaration(node) ||
          ts.isExportDeclaration(node)
        ) {
          if (
            !node.moduleSpecifier ||
            !ts.isStringLiteral(node.moduleSpecifier)
          )
            return null;
          importValue = node.moduleSpecifier.text;
        } else if (
          ts.isImportTypeNode(node) &&
          ts.isLiteralTypeNode(node.argument) &&
          ts.isStringLiteral(node.argument.literal)
        ) {
          importValue = node.argument.literal.text;
        } else if (ts.isModuleDeclaration(node)) {
          if (!ts.isStringLiteral(node.name)) return null;
          importValue = node.name.text;
        } else {
          return null;
        }

        const newImport = replaceAlias(
          fileName,
          importValue,
          compilerOptions.paths
        );

        if (!newImport || newImport === importValue) return null;

        const newSpec = factory.createStringLiteral(newImport);

        let newNode: ts.Node | null = null;

        if (ts.isImportTypeNode(node))
          newNode = factory.updateImportTypeNode(
            node,
            factory.createLiteralTypeNode(newSpec),
            node.assertions,
            node.qualifier,
            node.typeArguments,
            node.isTypeOf
            );

            if (ts.isImportDeclaration(node))
              newNode = factory.updateImportDeclaration(
              node,
              node.modifiers,
              node.importClause,
              newSpec,
              node.assertClause
            );

            if (ts.isExportDeclaration(node))
              newNode = factory.updateExportDeclaration(
              node,
              node.modifiers,
              node.isTypeOnly,
              node.exportClause,
              newSpec,
              node.assertClause
            );

            if (ts.isCallExpression(node))
              newNode = factory.updateCallExpression(
              node,
              node.expression,
              node.typeArguments,
              [newSpec]
            );

            if (ts.isModuleDeclaration(node))
              newNode = factory.updateModuleDeclaration(
              node,
              node.modifiers,
              newSpec,
              node.body
            );

            return newNode;
            }

            function visitor(node: ts.Node): ts.Node {
            	return traverseVisitor(node) || ts.visitEachChild(node, visitor, ctx);
            }
            return ts.visitNode(sourceFile, visitor);
            };
            }) as ts.TransformerFactory<ts.Bundle | ts.SourceFile>;
            }

参考資料#

https://www.youtube.com/watch?v=BU0pzqyF0nw
https://github.com/basarat/typescript-book
https://github.com/itsdouges/typescript-transformer-handbook
https://github.com/LeDDGroup/typescript-transform-paths
https://github.com/nonara/ts-patch
https://github.com/LeDDGroup/typescript-transform-paths/blob/v1.0.0/src/index.ts
https://github.com/microsoft/TypeScript-Compiler-Notes

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。