Rush Stack商店博客活动
跳到主要内容

架构说明

如果您有兴趣贡献,这里有一些通用的架构说明,希望可以帮助您了解代码库。(顺便说一下,如果您想查看有关特定方面或主题的更多详细信息,请通过在 rushstack-websites 文档单体存储库中创建一个 GitHib 问题来告知我们。)

项目结构

API 提取器的代码被分成反映子系统的源代码文件夹,这些子系统可以按一个粗略的整体操作流程来排列。

  • src/cli - 用于启动操作的命令行界面 (CLI)

  • src/api - 此文件夹包含公共 API,例如 ExtractorExtractorConfig。CLI 以外部使用者相同的方式调用这些 API;它不使用任何特殊的内部机制。TypeScript 编译器在此阶段进行配置,生成将在下面使用的 ts.Program 对象。

  • src/collector - Collector 充当中央协调器,运行下面许多阶段。从概念上讲,它是在一个中央位置“收集”所有 API 信息,主要是 CollectorEntity 对象。此文件夹还包含 MessageRouter 类,该类根据 api-extractor.json 中的 "messages" 表路由错误和警告。

  • src/analyzer - 核心分析器,它遍历 TypeScript 编译器的抽象语法树 (AST) 并生成 API 提取器使用的更高级别的表示形式。这里有 4 个主要的技术部分

    • AstSymbolAstDeclaration 类,它们镜像编译器的 ts.Symbolts.Declaration 类。不同之处在于,仅针对将成为文档网站及其 api-extractor-model 表示中的“API 项目”的特定节点子集(例如类、枚举、接口等)生成 AstDeclaration 节点。此压缩树省略了所有中间 ts.Declaration 节点(例如 extends 子句、: 令牌等)。AstSymbol.ts 代码注释提供了有关此非常重要的数据结构的更多详细信息。

    • ExportAnalyzer,它遍历 TypeScript import 语句链,消除中间符号别名以构建在 .d.ts 卷积中看到的扁平化视图。问题是,编译器的 API 使检测何时此遍历离开工作包(例如跳入 node_modules 文件夹或编译器的运行时库)变得很困难。这就是为什么此文件对每种导入语法都有特殊处理的原因。export * from 结构是迄今为止最复杂的形式。

    • Span 类,它是一个相当简单但相当有效的实用程序,用于重写 TypeScript 源代码,同时忽略其大部分含义,除了我们识别的特定节点类型。API 提取器不使用编译器的发射器来写入 .d.ts 文件,部分原因是在我们开始时这些 API 不是公开的,而且因为它们更忠实地保留了原始的 .d.ts 输入。 DtsRollupGenerator._modifySpan() 函数很好地说明了如何使用 Span

    • AstReferenceResolver:给定一个 TSDoc 声明引用,它会遍历 AstSymbolTable 以找到它引用的任何内容。

  • src/enhancers - 在 Collector 收集完所有 API 对象及其元数据后,我们会运行一系列称为 enhancers 的附加后处理阶段。当前阶段是 ValidationEnhancer(它应用一些 API 验证规则)和 DocCommentEnhancer,它会调整 TSDoc 注释,例如扩展 @inheritDoc 引用。

  • src/generators - 此文件夹实现了 API 提取器的著名 3 种输出类型:ApiReportGeneratorDtsRollupGeneratorApiModelGenerator

  • src/schemas - 此文件夹包含 api-extractor init 模板文件、api-extractor.json 的 JSON 架构以及 api-extractor-defaults.json,它代表 api-extractor.json 设置的默认值。

数据流

理解 API 提取器的另一种有用方法是检查声明在被每个阶段转换时会发生什么。考虑一个具有两个重载的简单 function 声明

import { Report } from 'reporting-package';

/** Declaration 1 */
export declare function add(report: Report, amount: number): void;

/** Declaration 2 */
export declare function add(report: Report, title: string): void;

以下是它的处理方式

  • 编译器阶段:TypeScript 编译器引擎将 .d.ts 文件解析为两个 ts.Declaration 对象(每个重载一个),它们代表解析的语法。编译器的分析器随后会生成一个关联的 ts.Symbol,它代表函数的类型。每个 TypeScript 类型始终只成为一个符号,在本例中,它与两个关联声明(两个重载)相关联。该符号还将有许多“别名”。例如,如果我们编写 import { add } from "./math",此处的 add 将成为一个符号别名,其声明是该 import 语句。如果我们按照符号别名链(可能通过许多导入和导出),我们总是会到达与 add() 的原始真实定义相对应的唯一“跟随符号”。

  • 分析器阶段:API 提取器从您的 API 入口点开始,并跟踪每个导出以找到其“跟随符号”。然后我们为 add() 生成一个 AstSymbol 和两个 AstDeclaration。分析器还会遍历 AST 树以填写上下文。例如,如果 AstSymbol 是一个 class,那么我们会为它的每个成员创建一个子 AstSymbol。如果该类属于一个 namespace,则会添加一个父 AstSymbol 来代表该命名空间。

    在跟踪 import 语句时,如果我们到达一个外部 NPM 包,则分析会在那里停止并生成一个 AstImport 而不是一个常规的 AstSymbol。这是因为 API 提取器了解包边界,并且实际上被设计为分别在每个项目上调用。因此,在上面的示例中,Report 将成为一个 AstImport 而不是一个 AstSymbol。分析器的总体工作是筛选极其详细的编译器数据结构并生成一个简化的 AstSymbol 对象树。此算法是 API 提取器中最复杂的阶段,因此我们尝试使其保持隔离和单一用途。

  • 收集器阶段:收集器构建了将最终成为 .d.ts 卷积中顶层项目的库存。我们称这些为 CollectorEntity 对象,对于我们的 add() 函数有一个,对于 Report 导入有一个。因此,AstSymbolAstImport 可以成为一个 CollectorEntity。但请注意,AstDeclaration 不能,AstModule(分析器对 .d.ts 源文件的表示)也不能。为了保持一致性,如果分析器对象可以成为一个 CollectorEntity,那么它们就从 AstEntity 基类继承。CollectorEntity 包装了 AstEntity 并添加了一些额外的收集器阶段信息

    • 该实体是 .d.ts 卷积的 export 还是仅一个本地声明。
    • .d.ts 卷积中的本地名称,因为本地声明可能需要由 DtsRollupGenerator._makeUniqueNames() 重命名以避免命名冲突
    • 导出名称,可以与本地名称不同。例如:export { A as B, A as C }
  • 增强器阶段:增强器主要使用 DeclarationMetadataApiItemMetadataSymbolMetadata 对象。这些对象存储在 AstSymbolAstDeclaration 上,但它们完全由收集器阶段拥有。

  • ApiReportGenerator 和 DtsRollupGenerator:这些生成器本质上只是将 CollectorEntity 项目转储到一个大型文本文件中,但格式不同。除了根据发布类型修剪项目之外,它们没有执行太多处理。

  • api-extractor-model 阶段:@microsoft/api-extractor-model 包完全独立,不依赖于上面描述的任何其他 API 提取器类型。它定义了可移植的 .api.json 文件格式。它有自己的丰富层次结构,继承自 ApiItem 基类(实际上是混合继承):ApiClassApiNamespaceApiParameter 等。在我们的示例中,add() 函数将成为此表示中的一个 ApiFunction 项目。此模型旨在使第三方能够轻松生成文档,而无需了解棘手的编译器数据结构。因此,ApiModelGenerator 会获取我们的 add()CollectorEntity 并将其转换为一个 ApiFunction,该函数将被序列化到 .api.json 中。

    回想一下,分析器在内部使用了 AstReferenceResolver 帮助程序来查找 TSDoc 声明引用并找到目标 AstDeclaration。对于 .api.json 文件,@microsoft/api-extractor-model 提供了类似的 ModelReferenceResolver 帮助程序,它可以查找 ApiItem 目标。

  • API 文档生成器阶段: 好了,这里发生了最后一次转换。这是最后一个了!:-) 当 API 文档生成器加载 .api.json 文件时,它不会直接将其渲染成 .md 文件。首先,它会将我们 add() 示例函数的 ApiFunction 转换为 TSDoc DocNode 元素的树。通常 DocNode 用于表示文档注释。但它恰好是一个完整的类似 DOM 的结构,可以表示富文本。由于 add() 的 TSDoc 注释已经是这种类型的富文本,API 文档生成器巧妙地重用了这种表示来模拟整个网页。这种中间表示使 markdown 发射器能够与文档引擎分离,并且在将来可以轻松地输出其他格式,例如 HTML 或 React。

总而言之,对于简陋的 add() 函数,此管道生成了许多不同的表示

  • AstDeclaration 用于重载声明
  • AstSymbol 用于 TypeScript 类型
  • CollectorEntity 用于 .d.ts 文件中的条目
  • DeclarationMetadataApiItemMetadataSymbolMetadata 用于使用更多信息来注释符号和声明
  • ApiFunction 用于 .api.json 文件
  • DocNode 子树用于文档网站