你如何可视化TypeScript依赖关系图?
大多数前端应用最终会发展成数百甚至数千个TypeScript文件。在你分析架构、检测循环依赖或构建开发工具之前,你需要回答一个非常简单的问题:

哪些文件依赖于哪些文件?
TypeScript编译器API已经包含了这些信息。每个源文件都被解析成抽象语法树(AST),导入语句成为可编程检查的节点。
像往常一样,完整功能的代码可以在 GitHub 仓库 中获取。
在本文中,我们将构建一个名为ts-graph的小型命令行工具,如何在 Windows 11/10 中禁用 Windows Defender - 致知笔记 - 专注教程知识分享 它扫描TypeScript项目并打印其依赖图。
最终输出将是这样的:
<span style="background-color:#f8f9fb"><span style="color:#4d4d4c"><span style="color:#660066">App</span><span style="color:#666600">.</span><span style="color:#000000">ts
</span><span style="color:#666600">├──</span> <span style="color:#660066">Home</span><span style="color:#666600">.</span><span style="color:#000000">ts
</span><span style="color:#666600">│</span> <span style="color:#666600">├──</span><span style="color:#000000"> homeMessages</span><span style="color:#666600">.</span><span style="color:#000000">ts
</span><span style="color:#666600">│</span> <span style="color:#666600">│</span> <span style="color:#666600">├──</span><span style="color:#000000"> textCase</span><span style="color:#666600">.</span><span style="color:#000000">ts
</span><span style="color:#666600">│</span> <span style="color:#666600">│</span> <span style="color:#666600">└──</span><span style="color:#000000"> clock</span><span style="color:#666600">.</span><span style="color:#000000">ts
</span><span style="color:#666600">│</span> <span style="color:#666600">└──</span><span style="color:#000000"> userSession</span><span style="color:#666600">.</span><span style="color:#000000">ts
</span><span style="color:#666600">│</span> <span style="color:#666600">└──</span><span style="color:#000000"> envFlags</span><span style="color:#666600">.</span><span style="color:#000000">ts
</span><span style="color:#666600">├──</span> <span style="color:#660066">Button</span><span style="color:#666600">.</span><span style="color:#000000">ts
</span><span style="color:#666600">│</span> <span style="color:#666600">└──</span><span style="color:#000000"> buttonStyles</span><span style="color:#666600">.</span><span style="color:#000000">ts
</span><span style="color:#666600">│</span> <span style="color:#666600">└──</span><span style="color:#000000"> designTokens</span><span style="color:#666600">.</span><span style="color:#000000">ts
</span><span style="color:#666600">├──</span><span style="color:#000000"> utils</span><span style="color:#666600">.</span><span style="color:#000000">ts
</span><span style="color:#666600">│</span> <span style="color:#666600">└──</span><span style="color:#000000"> textCase</span><span style="color:#666600">.</span><span style="color:#000000">ts
</span><span style="color:#666600">└──</span><span style="color:#000000"> theme</span><span style="color:#666600">.</span><span style="color:#000000">ts
</span><span style="color:#666600">├──</span><span style="color:#000000"> designTokens</span><span style="color:#666600">.</span><span style="color:#000000">ts
</span><span style="color:#666600">└──</span><span style="color:#000000"> envFlags</span><span style="color:#666600">.</span><span style="color:#000000">ts</span></span></span>
在此过程中,我们将介绍四个基本的编译器API概念:
- 程序 — 表示整个被分析的TypeScript项目
- SourceFile — 表示单个 TypeScript 文件及其 AST
- ImportDeclaration — 表示导入语句的 AST 节点

项目的理念是从你想分析的项目的入口文件开始创建一个程序对象(在仓库中,我们将使用示例目录);然后我们会在目标目录中运行 SourceFile 对象;这些将是我们依赖树的节点。为了了解哪些文件被导入,我们会分析每个SourceFile对象的AST,检查ImportDeclaration节点,最终构建依赖图。
如何创建 TypeScript 程序对象
编译器 API 总是从创建程序开始。如果AST是解析代码,那么程序就是拥有所有解析代码的部分。
在我们这个微型CLI中,这是核心配置:
<span style="background-color:#f8f9fb"><span style="color:#4d4d4c"><span style="color:#000088">const</span><span style="color:#000000"> program </span><span style="color:#666600">=</span><span style="color:#000000"> ts</span><span style="color:#666600">.</span><span style="color:#000000">createProgram</span><span style="color:#666600">(</span>
<span style="color:#666600">[],</span>
<span style="color:#666600">{}</span>
<span style="color:#666600">);</span></span></span>
一旦你有了程序对象,就可以访问该方法,它会返回所有加载到TypeScript程序中的源文件。这意味着它包含目标样本目录的文件、所选的入口文件及其导入的依赖,以及TypeScript库声明文件和编译器解析带来的任何额外文件。在接下来的几行中,我们将筛选这些条目,只保留包含你想分析项目文件夹的文件用于图构建,而跳过外部和标准库文件。Program.getSourceFiles()
即使你的最终目标是“只是解析导入”,如何在 Windows 11/10 中删除重复照片 - 致知笔记 - 专注教程知识分享 你仍然从这里开始。你得到一个项目的统一模型,而不是试图逐个拼凑文件解析。
如何在TypeScript项目中遍历源文件
SourceFile 表示单个解析后的 TypeScript 文件,并作为其抽象语法树(AST)的根节点。在我们的CLI中,我们从程序中检索这些文件,忽略声明文件,因为我们只关注应用程序源代码:
<span style="background-color:#f8f9fb"><span style="color:#4d4d4c"><span style="color:#000088">const</span><span style="color:#000000"> sourceFiles </span><span style="color:#666600">=</span><span style="color:#000000"> program
</span><span style="color:#666600">.</span><span style="color:#000000">getSourceFiles</span><span style="color:#666600">()</span>
<span style="color:#666600">.</span><span style="color:#000000">filter</span><span style="color:#666600">(</span><span style="color:#000000">sf </span><span style="color:#666600">=></span> <span style="color:#666600">!</span><span style="color:#000000">sf</span><span style="color:#666600">.</span><span style="color:#000000">isDeclarationFile</span><span style="color:#666600">);</span></span></span>
每个源文件包含一个 AST 节点组成的树状结构,代表代码的结构。我们可以递归遍历这棵树,寻找特定节点——在我们这里,是ImportDeclaration节点,告诉我们源文件依赖哪些其他文件。
这种简单的模式——走动AST,收集有用信息——是许多编译器API工具的核心。Linter、codemod、静态分析器和架构工具都采用相同方法的变体来检查源代码并提取有意义的信号。
如何从 TypeScript AST 中提取导入声明
为了遍历 AST,TypeScript 编译器 API 使用一个简单的访问者模式:我们定义一个函数接收节点,检查它是否包含我们感兴趣的信息,然后继续通过其子节点进行遍历。
对于我们的依赖图,我们关注的是 ImportDeclaration 节点,因为它们代表导入语句。因此,访客可以保持极其小巧:
<span style="background-color:#f8f9fb"><span style="color:#4d4d4c"><span style="color:#000088">function</span><span style="color:#000000"> visit</span><span style="color:#666600">(</span><span style="color:#000000">node</span><span style="color:#666600">:</span><span style="color:#000000"> ts</span><span style="color:#666600">.</span><span style="color:#660066">Node</span><span style="color:#666600">)</span> <span style="color:#666600">{</span>
<span style="color:#000088">if</span> <span style="color:#666600">(</span><span style="color:#000000">ts</span><span style="color:#666600">.</span><span style="color:#000000">isImportDeclaration</span><span style="color:#666600">(</span><span style="color:#000000">node</span><span style="color:#666600">))</span> <span style="color:#666600">{</span>
<span style="color:#880000">// extract dependency</span>
<span style="color:#666600">}</span><span style="color:#000000">
ts</span><span style="color:#666600">.</span><span style="color:#000000">forEachChild</span><span style="color:#666600">(</span><span style="color:#000000">node</span><span style="color:#666600">,</span><span style="color:#000000"> visit</span><span style="color:#666600">);</span>
<span style="color:#666600">}</span></span></span>
调用是使函数递归的原因。每个AST节点都可以有子节点,因此在检查当前节点后,我们只需请求TypeScript使用相同的函数访问每个子节点。如何在 Windows 11/10 中创建访客用户帐户 - 致知笔记 - 专注教程知识分享因此,访客可以完整地走过整个AST,而我们无需了解每一个可能的TypeScript结构的结构。ts.forEachChild()
针对我们的具体问题,作为过滤器:每当遍历遇到导入语句时,我们找到了一个可以添加到图中的依赖。ts.isImportDeclaration()
这段小代码捕捉了使用编译器API的核心:走过树,识别你关心的节点,并从中提取信息。
如何构建和结构依赖映射
现在我们已经拥有构建依赖图所需的所有部分。过程很简单:对于程序中的每个源文件,我们遍历其 AST 并寻找 ImportDeclaration 节点。每次导入代表当前文件到另一个文件的一条边,我们会将其存储在图中。
最简单的算法核心如下:
<span style="background-color:#f8f9fb"><span style="color:#4d4d4c"><span style="color:#000088">for</span> <span style="color:#666600">(</span><span style="color:#000088">const</span><span style="color:#000000"> sourceFile </span><span style="color:#000088">of</span><span style="color:#000000"> program</span><span style="color:#666600">.</span><span style="color:#000000">getSourceFiles</span><span style="color:#666600">())</span> <span style="color:#666600">{</span>
<span style="color:#000088">const</span><span style="color:#000000"> dependencies</span><span style="color:#666600">:</span><span style="color:#000000"> string</span><span style="color:#666600">[]</span> <span style="color:#666600">=</span> <span style="color:#666600">[];</span><span style="color:#000000">
visitNode</span><span style="color:#666600">(</span><span style="color:#000000">sourceFile</span><span style="color:#666600">,</span><span style="color:#000000"> dependencies</span><span style="color:#666600">);</span><span style="color:#000000">
graph</span><span style="color:#666600">.</span><span style="color:#000088">set</span><span style="color:#666600">(</span><span style="color:#000000">sourceFileName</span><span style="color:#666600">,</span><span style="color:#000000"> dependencies</span><span style="color:#666600">);</span>
<span style="color:#666600">}</span></span></span>
访客才是实际的提取。每当遇到 ImportDeclaration,我们就将其模块指定符添加到依赖列表中:
<span style="background-color:#f8f9fb"><span style="color:#4d4d4c"><span style="color:#000088">function</span><span style="color:#000000"> visitNode</span><span style="color:#666600">(</span><span style="color:#000000">node</span><span style="color:#666600">:</span><span style="color:#000000"> ts</span><span style="color:#666600">.</span><span style="color:#660066">Node</span><span style="color:#666600">,</span><span style="color:#000000"> dependencies</span><span style="color:#666600">:</span><span style="color:#000000"> string</span><span style="color:#666600">[])</span> <span style="color:#666600">{</span>
<span style="color:#000088">if</span> <span style="color:#666600">(</span><span style="color:#000000">ts</span><span style="color:#666600">.</span><span style="color:#000000">isImportDeclaration</span><span style="color:#666600">(</span><span style="color:#000000">node</span><span style="color:#666600">))</span> <span style="color:#666600">{</span><span style="color:#000000">
dependencies</span><span style="color:#666600">.</span><span style="color:#000000">push</span><span style="color:#666600">(</span><span style="color:#000000">node</span><span style="color:#666600">.</span><span style="color:#000000">moduleSpecifier</span><span style="color:#666600">.</span><span style="color:#000000">text</span><span style="color:#666600">);</span>
<span style="color:#666600">}</span><span style="color:#000000">
ts</span><span style="color:#666600">.</span><span style="color:#000000">forEachChild</span><span style="color:#666600">(</span><span style="color:#000000">node</span><span style="color:#666600">,</span><span style="color:#000000"> child </span><span style="color:#666600">=></span><span style="color:#000000"> visitNode</span><span style="color:#666600">(</span><span style="color:#000000">child</span><span style="color:#666600">,</span><span style="color:#000000"> dependencies</span><span style="color:#666600">));</span>
<span style="color:#666600">}</span></span></span>
最终的数据结构刻意简化:一个映射,每个文件对应其导入的文件。
<span style="background-color:#f8f9fb"><span style="color:#4d4d4c"><span style="color:#660066">Map</span><span style="color:#666600"><</span><span style="color:#000000">string</span><span style="color:#666600">,</span><span style="color:#000000"> string</span><span style="color:#666600">[]></span></span></span>
例如,在App.ts中的导入可以表示为:
<span style="background-color:#f8f9fb"><span style="color:#4d4d4c"><span style="color:#660066">App</span><span style="color:#666600">.</span><span style="color:#000000">ts </span><span style="color:#666600">→</span> <span style="color:#666600">[</span><span style="color:#660066">Home</span><span style="color:#666600">.</span><span style="color:#000000">ts</span><span style="color:#666600">,</span> <span style="color:#660066">Button</span><span style="color:#666600">.</span><span style="color:#000000">ts</span><span style="color:#666600">,</span><span style="color:#000000"> utils</span><span style="color:#666600">.</span><span style="color:#000000">ts</span><span style="color:#666600">]</span></span></span>
这里没有复杂的图算法或特殊的数据结构。有趣的不是依赖是如何存储的,而是我们如何从AST中提取它们。修复:无法在 Adobe Reader 中打印 PDF 文件 - 致知笔记 - 专注教程知识分享编译器API为我们提供了源代码的结构;我们的访客将这种结构转化为我们可以使用的信息。
这就是该工具的基本模式:程序允许我们访问项目,每个源文件提供一个AST,访问者提取ImportDeclaration节点,这些导入成为依赖图中的边。有了这个图表后,我们就可以开始做一些有用的事情了。
结论
这个代码很小,但它不是那种轻视意义上的玩具。它是严肃工具制作的第一个真正基石。
依赖图是许多开发工具的基础:架构可视化工具、循环依赖检测器、未使用模块分析、影响分析、捆绑器和集成开发(IDE)功能,都是从理解源文件相互关系开始的。
仓库中的代码设计成实现一个合适的 shell 命令:它会接收包含你想分析项目的目录的参数,所以即使很简单,它也是更复杂功能的合格模板。
从这里开始,我们可以通过添加额外的元数据(如导出的符号)来丰富图,从而从文件层级的依赖转向符号层级的关系。这一小小的步骤为在 TypeScript 编译器 API 之上构建更复杂工具打开了大门。

浙公网安备 33010602011771号