banner
Madinah

Madinah

github
twitter
telegram
email
bilibili

TS 編譯 API

背景#

之前在進行 SDK 編譯流程的時候,為了方便開發者開發,經常會寫一些 alias 方便將一些長的相對路徑變成一個個很短的 alias 之後的路徑。
例如

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

這樣的作用是將別名永遠指向 src 目錄,當 src 目錄下面層級非常深的文件引用頂層的文件的時候,可以直接寫成,這樣可以減少相對路徑的引用,讓代碼看起來美觀 && 方便調整文件的目錄結構。

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

這樣的話開發階段的 DX 體驗友好,但是當我們最終把代碼打包成 SDK 的時候,使用 SDK 的宿主環境不一定和我們配置成一樣的 alias,不一致的話就會存在找不到文件的問題,這就需要我們在打包 TS 的時候將 alias 的相關文件編譯回相對路徑,以實現所有引入方的兼容。這樣就需要對 TS 的編譯流程有個了解。

TS compiler 相關#

TS 的編譯流程#

SourceCode(源碼) ~~ 掃描器(Scanner) ~~> Token 流
Token 流 ~~ 解析器 (parser) ~~> AST(抽象語法樹)
AST ~~ 綁定器 (binder) ~~> Symbols(符號)
AST + 符號(Symbols) ~~ 檢查器 ~~> 類型驗證
AST + 檢查器 ~~ 發射器(emitter) ~~> JavaScript 代碼

掃描器#

TS(TypeScript)中的 scanner(掃描器)是編譯器的第一個階段,也被稱為詞法分析器。它負責將源代碼文件中的字符流轉換成一系列的詞法單元(tokens)。
工作方式如下:

  1. 讀取字符流: scanner 從源代碼文件中逐個讀取字符。
  2. 識別詞法單元: scanner 根據一組預定義的語法規則,將字符組合成識別出的詞法單元,如標識符、關鍵字、運算符、常量等。它使用有限自動機(finite automation)或正則表達式來匹配字符序列。
  3. 生成詞法單元:一旦識別出一個完整的詞法單元,scanner 將其生成为一個包含類型和值信息的對象,並將其傳遞給下一個階段的編譯器。
  4. 處理特殊情況: scanner 同時處理特殊情況,如註釋、字符串字面量,以及對轉義字符的解析等。

例如,考慮以下 TypeScript 代碼片段:

let age: number = 25;

scanner 將逐個讀取字符並生成以下詞法單元:

  1. let 關鍵字
  2. age 標識符
  3. : 冒號(運算符)
  4. number 關鍵字
  5. = 等號(運算符)
  6. 25 數字常量
  7. ; 分號(分隔符)

詞法單元生成的順序由語法規則定義,scanner 會不斷重複這個過程,直到源代碼文件中的所有字符都被處理完畢。這個階段只是將相關的 token 提取出來,沒有進行語法,語義相關的分析。

import * as ts from "typescript";

// TypeScript has a singleton scanner
const scanner = ts.createScanner(ts.ScriptTarget.Latest, /*skipTrivia*/ true);

// That is initialized using a function `initializeState` similar to
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);
}

// Sample usage
initializeState(`
var foo = 123;
`.trim());

// Start the scanning
var token = scanner.scan();
while (token != ts.SyntaxKind.EndOfFileToken) {
  console.log(ts.SyntaxKind[token]);
  token = scanner.scan();
}

output

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);

output

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 ~~Scanner~~> Tokens ~~Parser~~> AST ~~Emitter~~> JavaScript

但是上面的流程對於 TS 來說少了一個關鍵的步驟 TypeScript 的語義** 系統,** 為了協助(檢查器執行)類型檢查,綁定器將源碼的各部分連接成一個相關的類型系統,供檢查器使用。綁定器的主要職責是創建_符號_(Symbols)

  1. 簡單理解

image

  1. 深入探索結構

image

image.png#

可以通過裡面的 pos end 來判斷作用域相關的引用唯一性

檢查器#

這裡會聯合上面綁定器產生出來的 Symbol 一起做類型推導、類型檢查等
代碼示例

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 = ({)

output

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,
    ),
  );
}

Transformers#

上面的部分介紹了 TS 編譯代碼的一些流程,同時 TS 也給我們提供了一些類似於 “生命週期的鉤子”,允許我們在編譯的流程中添加自己自定義的部分。

  • before 在 TypeScript 之前運行轉換器(代碼還沒有編譯)
  • after 在 TypeScript 之後運行轉換器(代碼已編譯)
  • afterDeclarations 在聲明步驟之後運行轉換器(你可以在這裡轉換類型定義)

API#

visiting#

  • ts.visitNode (node, visitor) 用來遍歷 root node
  • ts.visitEachChild (node, visitor, context) 用來遍歷子節點
  • ts.isXyz (node) 用來判斷節點類型的 例如 ts.isVariableDeclaration (node)

Nodes#

  • ts.createXyz 創建新節點(然後返回),ts.createIdentifier ('world')
  • ts.updateXyz 用來更新節點 ts.updateVariableDeclaration ()

寫一個 transformer#

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

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。