源码跳转
在 monorepo 中,包源码通常位于同一仓库不同目录下。
─ projects
├── parent
└── child
Coding 时,我们常需要点击跳转,查看源码。
- 直接点击跳转 → DTS 文件,这不是我们想要的。
- 全局搜索定义 → 倒也可以,就是有些低效了。
如果能直接跳转到源码查看,开发体验势必更上一层楼。基于这个目的,TS 在 3.0 版本之后支持了 Project References 特性,它允许我们将 TS 项目分割成若干个模块,彼此独立,这与 monorepo 的使用场景和理念相似。
下面是一份开启 references 的示例配置:
// parent/tsconfig.json
{
"compilerOptions": {},
// Usual config
"references": [
{ "path": "../child" }
]
}
在 TS 语言服务的作用下,编辑器(如 vscode)将具备源码跳转能力,效果大概是这样的:
不配置则跳转产物,而非源码
开启 Project References 非常简单,但想要在开启的基础上实现源码跳转,我们还有一些配置需要完善。
实现原理
举个栗子🌰,假设我们在 <root>/projects
目录下分别有 @project/parent
、@project/child
两个项目,其中父项目以 workspace 依赖的方式引用子项目。
{
"name": "@projects/parent",
"dependencies": {
"@project/child": "workspace:*"
}
}
在 projects/parent/src/index.ts
中,会有如下代码
import { xxx } from "@project/child";
项目 child 的 packages.json
中 types
配置如下
{
"name": "@projects/child",
"types": "dist/src/index.d.ts"
}
那么在 parent 没有配置 references 时,引用路径等效为
import { xxx } from "@project/child/dist/src/index.d.ts";
配置 references 后,在 TS Server 作用下,引用会被重定向到源码,如下
import { xxx } from "<root>/projects/child/src/index.ts";
这就是 references 的第一个作用——源码跳转,开启 Project References 的项目需要满足以下条件,以确保 TS 能够快速找到引用项目的输出内容:
- 父子项目需同时启用
composite
和declaration
- 子项目需正确设置
compilerOptions.rootDir
,通常设置为"src"
- 子项目需确保
tsconfig.json
中的输出目录(outDir
或declarationDir
)与package.json
中声明的types
对应。
以上步骤,是为了确保 TS Server 能够找到引用项目(也就是子项目)的输出内容。 而上面👆提到的「输出内容」指,TS 如何查找引用项目的源码,举个栗子🌰:
import { xxx } "@project/child"; // 源码在 projects/child/src 下
import { xxx } "<root>/projects/child"; // 配置了 references 后的效果,但还缺少 src 层级
import { xxx } "<root>/projects/child/src"; // ✅ 配置 rootDir: "src" 后正确加载到了源码
产物层级调整
我们通常只需要构建 src
下的内容,而 TSC 构建的产物结构却是与源目录结构一致的。如果你觉得多了一个 dist/src
层级不够优雅,想将 package.json
中的 types
配置成 dist/index.d.ts
,那么你可以通过配置 rootDir 和 include 来实现。
child 项目的 tsconfig 修改之前长这样:
{
"compilerOptions": {
"outDir": "dist",
}
}
修改后,增加 rootDir 和 include 配置:
{
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
为了确保源码跳转正常,我们还需要修改 child 项目的 package.json,去掉 src
以维持「对应关系」
{
"name": "@projects/child",
"types": "dist/index.d.ts"
}
调整完成后,回到上文中的例子,在 parent 没有配置 references 时,引用路径等效为
import { xxx } from "@project/child/dist/index.d.ts"; // ✅ 没有了 src
配置 references 后,由于 child 项目的 outDir 为 dist,删除 dist 后理论上应该重定向到 child/index.ts
而非 child/src/index.ts
,但因为配置了 rootDir
,TS Server 依旧能正常识别
import { xxx } from "<root>/projects/child/src/index.ts"; // 大功告成
对应关系
通常我们的 package.json 长这样:
{
...
"types": "./dist/types/index.d.ts",
...
}
tsconfig.json 长这样:
{
"compilerOptions": {
"outDir": "dist",
}
}
上面的配置是无法被正常映射的,需要保证 types 中的目录和 TS 类型的输出目录一致。
下列操作三选一:
- 将
compilerOptions.outDir
修改为dist/types
- 或将
compilerOptions.declarationDir
设置为dist/types
- 或将
package.json
中的types
设置为./dist/index.d.ts
实际应用时,大家灵活配置,统一即可。
推荐配置:
package.json
{
...
"types": "./dist/types/index.d.ts",
...
}
tsconfig.json
{
"compilerOptions": {
"outDir": "dist",
"declarationDir": "dist/types",
}
}
类型联合校验
当我们将项目彼此关联起来之后,它们会构成一个整体,在 TS 语言服务的作用下,可赋予编辑器直接跳转到源码的能力,非常便利。
但除此之外,Project References 还可以辅助我们在仓库内进行大范围的类型联合校验,这便是 references 的第二个作用,它在 CI 中有着积极意义。
我们可以在仓库根目录下(位置随意),新建一个空的 tsconfig.json 文件,作为构建入口,更多信息可以参考 overall-structure 。
// <root>/tsconfig.json
{
"files": [], // 空文件,全量校验使用
"references": [
{ "path": "projects/a" },
{ "path": "projects/b" },
{ "path": "projects/c" },
]
}
然后运行以下命令:
npx tsc --build tsconfig.json --force --verbose
在 build 模式下,tsc 会按照依赖关系,拓扑构建引用链中的所有项目,从而实现了类型联合校验。
错误治理
历史项目往往存在大量 TS 错误,导致校验不通过。为此,我们可以使用 ts-migrate 批量忽略存量 TS 错误,然后再开启联合校验。