- 开发工具
【免费下载链接】ts-morph
TypeScript Compiler API wrapper for static analysis and programmatic code changes.
导读
本篇指南围绕 ts-morph 中「如何在 Source File 已添加到 Project 之后获取它们」这一核心环节展开,系统讲解getSourceFiles(全部与 glob 过滤)、getSourceFile(按路径/按条件)以及getSourceFileOrThrow三种 API 的使用方式、匹配语义与底层实现。读完本文,你将能准确回答「在什么情况下会命中哪些文件」这一最关键的问题,避免因当前工作目录(CWD)、相对路径与 glob 排除模式使用不当而取错文件,并理解这些行为背后对应仓库中的哪一段实现代码。
为什么需要单独讨论「获取」Source Files
在 ts-morph 中,Project 是管理所有已加载源码文件的中心对象。无论通过addSourceFilesAtPaths、addSourceFilesFromTsConfig添加文件,还是通过createSourceFile创建内存中的文件(对应 Project.ts),这些文件都会被登记到 Project 内部。而「获取」则是一个独立环节:正如原文档(getting-source-files.md)所指出的——「After source files are added, you will need to get them in order to navigate or make changes」,即只有先稳定地取回SourceFile对象,才能继续做节点遍历(Navigation)或程序化修改(Manipulation)。
因此本文所有 API 都建立在文件已添加的前提之上,讨论重点不是「怎么添加」,而是「如何精确、高效地把目标文件取回来」。
获取全部 Source Files
最简单的情形是获取当前 Project 中的全部源码文件,直接调用无参的getSourceFiles():
const sourceFiles = project.getSourceFiles();返回类型为SourceFile[]。从源码看,该无参重载(Project.ts)内部会调用#getProjectSourceFilesByDirectoryDepth()生成器:它遍历编译器工厂中的所有源文件,并通过inProjectCoordinator.isSourceFileInProject(sourceFile)过滤,只返回真正属于当前 Project 的文件。这一过滤非常重要——那些仅被 TypeScript 编译器解析进来、但并未被显式标记为「属于项目」的依赖文件不会混入结果。
从源码结构看,遍历顺序并非随机:测试 projectTests.ts 验证了结果按目录深度排序(同一目录下大写字母优先,随后按目录层级由浅入深)。这意味着当你需要稳定的文件顺序做批处理时,可以依赖该顺序。
使用 glob 模式过滤获取
getSourceFiles还支持传入一个或多个 glob 模式进行过滤,这在只关心某类文件(例如测试文件)时非常实用:
// 单个 glob const testSourceFiles = project.getSourceFiles("src/test/**/*.ts"); // 多个 glob(含排除模式) const nonTestSourceFiles = project.getSourceFiles([ "src/**/*.ts", "!src/test/**/*.ts", ]);单模式形式直接按模式筛选;数组形式中,!前缀表示「否定/排除」模式。底层实现位于 matchGlobs.ts:它会依次遍历每个文件路径与每个模式,先用FileUtils.toAbsoluteGlob把(相对)模式转换为基于当前目录的绝对 glob,再用FileUtils.isNegatedGlob识别!前缀,命中否定模式时从结果中移除该路径。因此多个模式之间是叠加的过滤过程:先加入匹配项,再用排除项剔除。
仓库测试给出了一个多模式叠加的完整示例(projectTests.ts):
project.getSourceFiles(["**/src/**/*.ts", "!**/src/test/**/*.ts", "!**/*.d.ts"]) // 结果仅剩 /src/file.ts在创建了file.ts、src/file.ts、src/test/file1.ts、src/test/file1.d.ts、src/test/file2.ts、src/test/file3.ts、src/test/file3.js、src/test/folder/file.ts等文件的前提下:
**/src/**/*.ts先命中所有 src 下的 ts/d.ts 文件;!**/src/test/**/*.ts剔除 src/test 目录下的全部 .ts/.d.ts;!**/*.d.ts进一步剔除声明文件;- 最终只剩
src/file.ts。
这一组合写法是「保留大部分、剔除特定子集」的标准模式,可直接套用到实际项目。
路径解析基于当前工作目录(重点)
原文档特别强调了一个易错点:路径解析与模式匹配都是相对于当前工作目录(current working directory,CWD)进行的。
const project = new Project({ tsConfigFilePath: `/someDirectory/notCurrent/tsconfig.json`, });如果 tsconfig 位于当前目录之外的/someDirectory/notCurrent/,那么想要匹配该目录下的文件,就不能写基于 tsconfig 所在目录的相对 glob,而必须写出「相对于当前工作目录或绝对路径」的模式:
const sourceFile = project.getSourceFiles(`/someDirectory/notCurrent/**/config/index.ts`);从源码印证:matchGlobs在匹配前会以fileSystemWrapper.getCurrentDirectory()作为cwd调用FileUtils.toAbsoluteGlob将模式标准化为绝对 glob(见 matchGlobs.ts 与 FileUtils.ts)。也就是说,模式中的相对路径全部以 CWD 为基准解析,而与 Project 内部保存的 tsconfig 路径没有直接关系。跨目录加载项目时,最稳妥的做法是直接使用绝对路径模式,避免一切歧义。
按文件路径获取:返回第一个「路径结尾匹配」的文件
getSourceFile(filePath)用于按路径获取单个源文件,返回SourceFile | undefined。其语义是原文档描述的关键:返回第一个「文件路径以给定路径结尾」匹配的文件:
const personFile = project.getSourceFile("Models/Person.ts");底层行为(Project.ts)分为两种情况:
- 传入纯文件名(不含
/):例如getSourceFile("file.ts"),会被包装成一个谓词def => FileUtils.pathEndsWith(def.getFilePath(), fileNameOrPath),即对每个源文件逐一做「路径结尾匹配」,返回第一个命中的文件。FileUtils.pathEndsWith(FileUtils.ts)会按/切分路径片段后从尾部逐一比对,因此"Models/Person.ts"、"Person.ts"这类片段式写法都能命中".../Models/Person.ts"。 - 传入含
/的路径或绝对路径:会被FileUtils.standardizeSlashes规范化并通过getStandardizedAbsolutePath解析为绝对路径,随后直接从编译器缓存中按完整路径精确查找。值得注意的是源码注释明确说明「when a file path is specified, return even source files not in the project」——按完整路径查找时,即使该文件未标记为项目内文件也会返回。
需要特别留意「返回第一个」的含义:如果 Project 中存在dir/file.ts与file.ts两个文件,getSourceFile("file.ts")会返回哪个?测试 projectTests.ts 给出了答案——取决于两个文件被创建的先后顺序(即目录深度遍历顺序),并验证了./file.ts、dir/../dir/file.ts、/file.ts等多种写法都能被正确处理。因此当同名文件可能冲突时,建议使用更完整的路径(含目录或绝对路径)来消除歧义。
按条件获取:谓词搜索
当「按路径」无法表达你的查找意图时,可以传入一个谓词函数,getSourceFile将返回第一个满足条件的源文件:
const fileWithFiveClasses = project.getSourceFile(f => f.getClasses().length === 5);该重载的实现在getSourceFile(fileNameOrSearchFunction: string | ((file: SourceFile) => boolean))中:若参数是函数,则直接作为谓词交给IterableUtils.find对源文件集合进行线性查找,找到第一个返回true的文件(Project.ts)。谓词可以组合任意 SourceFile 能力,例如「文件名包含某个关键词」「是否处于 ambient 环境」「包含特定数量的接口」等。因为只返回第一个命中项,若你需要收集所有符合条件的文件,应改用getSourceFiles+ glob,或自行遍历getSourceFiles()后过滤。
找不到时的强约束版本:getSourceFileOrThrow
getSourceFile在找不到文件时返回undefined,需要你自行判空。若希望「找不到就立即抛错」,可以使用强约束版本getSourceFileOrThrow:
const mainFile = project.getSourceFileOrThrow("main.ts");其行为(Project.ts)为:内部先调用getSourceFile,若结果为null则抛出InvalidOperationError,并且错误信息会根据输入形式区分三类:
- 传入纯文件名:报错
Could not find source file in project with the provided file name: <name>; - 传入相对/绝对路径:先标准化为绝对路径再报错
Could not find source file in project at the provided path: <path>; - 传入谓词函数:报错
Could not find source file in project based on the provided condition.。
测试 projectTests.ts 对这四种错误路径均有断言。在编写批处理脚本时,getSourceFileOrThrow能让「文件缺失」这类配置问题在第一时间暴露,而不是等到下游访问undefined时才抛出难以定位的异常。
从源码理解整体匹配流程
综合上述源码,一次「获取源文件」请求的完整链路可归纳为:
- 无参
getSourceFiles():从compilerFactory.getSourceFilesByDirectoryDepth()取得全部源文件,再经InProjectCoordinator过滤出项目内文件(Project.ts); - 带 glob:先收集项目内文件的路径,再由
matchGlobs以 CWD 为基准将模式转绝对 glob 并做「加入/排除」过滤,最后按匹配路径从缓存取回SourceFile(Project.ts、matchGlobs.ts); - 按路径/按条件:路径形式经标准化后走缓存精确查找(含项目外文件),纯文件名形式走
pathEndsWith结尾匹配,函数形式走谓词线性查找。
这套设计意味着:匹配始终以「Project 当前持有哪些源文件」为候选集(路径精确查找除外)。如果你先后addSourceFilesAtPaths后又removeSourceFile或调用了forget,候选集也随之变化——测试 projectTests.ts 专门验证了「不属于项目的文件不会被getSourceFiles返回」。因此在使用这些 API 前,先确认候选文件确实已被添加进 Project,是最常见的排错起点。
实战小结与最佳实践
- 拿全部:无参
getSourceFiles(),返回按目录深度排序的项目内全部源文件。 - 拿子集:
getSourceFiles(["src/**/*.ts", "!src/test/**/*.ts"]),用排除模式剔除目标目录;多模式按「先加入、后剔除」的顺序生效。 - 拿单个:
getSourceFile("Models/Person.ts")做路径结尾匹配并返回第一个命中;同名冲突时改用含目录的完整路径或绝对路径。 - 拿不到就报错:改用
getSourceFileOrThrow(...),让文件缺失在第一时间暴露,且错误信息区分文件名/路径/条件三种形态。 - 按逻辑拿:
getSourceFile(f => ...)用谓词表达无法用路径描述的条件。 - 跨目录项目:glob 始终相对 CWD 解析,涉及 tsconfig 在其他目录的 Project 时,使用绝对路径模式最安全。
如果你还需要按目录层级组织地获取文件(例如拿取某个目录下的全部源文件、处理目录级批量操作),可以继续阅读仓库中的 directories.md;而若需要基于语言服务做跨文件分析(如查找引用、语义诊断),可参考 program.md 与 type-checker.md。掌握了「添加—获取—遍历—修改」这条主线,你就能用 ts-morph 编写出稳定、可预测的程序化代码改造工具。
- 开发工具
【免费下载链接】ts-morph
TypeScript Compiler API wrapper for static analysis and programmatic code changes.
相关推荐
uni-app 宽屏适配完全指南:leftWindow/rightWindow/topWindow、match-media 组件与 rpx 缩放策略
uni app 宽屏适配完全指南:leftWindow/rightWindow/topWindow、match media 组件与 rpx 缩放策略 uni a
开发工具Penrose Style Selector 完全指南:语法、匹配算法与实战模式
Penrose Style Selector 完全指南:语法、匹配算法与实战模式 导读 本文以 Penrose 开源仓库中 Style Selector 参考文
开发工具数据可视化Compromise 匹配语法(match-syntax)完全指南:词级 Pattern、`Tag` 谓词与捕获组实战
Compromise 匹配语法(match syntax)完全指南:词级 Pattern、 Tag 谓词与捕获组实战 本文是 compromise(一个"低调的
NLP人工智能
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考