@typescript-eslint/project-service深度解析:基于 TypeScript Project Service 的类型化 Linting 引擎
【免费下载链接】typescript-eslint:sparkles: Monorepo for all the tooling which enables ESLint to support TypeScript项目地址: https://gitcode.com/GitHub_Trending/ty/typescript-eslint
本文围绕 typescript-eslint 仓库中的独立包@typescript-eslint/project-service展开,讲解它如何包装 TypeScript 官方的 Project Service API(即 VS Code 等编辑器在背后使用的"打开文件并生成类型信息程序"的机制),为 ESLint 提供类型化 Linting 所需的类型信息。读完本文,你将掌握createProjectService的完整 API 与返回结构、四个核心配置选项(allowDefaultProject、defaultProject、loadTypeScriptPlugins、maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING)的语义与限制,以及它如何被@typescript-eslint/typescript-estree解析器集成进parserOptions.projectService的完整调用链。
一、什么是 Project Service:编辑器背后的类型引擎
在深入代码之前,先理解一个关键背景:TypeScript 的Project Service是一组面向语言服务(Language Service)的 API,VS Code、Vim、WebStorm 等编辑器正是通过它来"程序化地"打开文件、维护项目配置、并在需要时生成 TypeScript 的Program(程序对象,承载完整的类型信息)。它与tsc按 tsconfig 一次性编译的模型不同,更像一个长期运行的"常驻服务",按需惰性构建项目与程序。
@typescript-eslint/project-service是这套 API 的独立导出包装器(Standalone wrapper),它在 packages/project-service/README.md 中被明确定位为:
为 typescript-eslint 的 typed linting 提供动力的 "Project Service" 的独立导出。
也就是说,这个包既服务于 typescript-eslint 内部的类型化规则,也可被任何希望"用编辑器同一套类型信息来源做静态分析"的工具直接使用。
二、快速上手:最小可运行示例
官方文档 docs/packages/Project_Service.mdx 给出了一个完整的最小示例,它演示了完整的"创建服务 → 打开文件 → 拿到 Program"三步流程:
import { createProjectService } from '@typescript-eslint/project-service'; const filePathAbsolute = '/path/to/your/project/index.ts'; const { service } = createProjectService(); service.openClientFile(filePathAbsolute); const scriptInfo = service.getScriptInfo(filePathAbsolute)!; const program = service .getDefaultProjectForFile(scriptInfo.fileName, true)! .getLanguageService(true) .getProgram()!;这段代码的每一行都对应一个关键概念:
| 步骤 | API 调用 | 作用 |
|---|---|---|
| 1 | createProjectService() | 创建 Project Service 实例及其元数据 |
| 2 | service.openClientFile(filePathAbsolute) | 让服务"打开"该文件,自动解析其所属项目(最近的 tsconfig.json) |
| 3 | service.getScriptInfo(filePathAbsolute) | 获取文件的 ScriptInfo(脚本信息)对象 |
| 4 | getDefaultProjectForFile(fileName, true) | 定位该文件所属的默认项目 |
| 5 | getLanguageService(true).getProgram() | 同步获取语言服务并取出包含完整类型信息的Program |
注意这里的service类型是ts.server.ProjectService(TypeScript 语言服务端的内部类),因此该包对外导出的核心类型TypeScriptProjectService正是它的别名——这一点可在 createProjectService.ts 的源码注释中确认。
三、createProjectService完整 API 与返回结构
该包的公共 API 只有一个函数createProjectService与相关类型,全部从 index.ts 导出(export * from './createProjectService')。它的签名定义在 createProjectService.ts:
export function createProjectService({ host, jsDocParsingMode, options: optionsRaw = {}, tsconfigRootDir, }: CreateProjectServiceSettings = {}): ProjectServiceAndMetadata3.1 入参CreateProjectServiceSettings
| 参数 | 类型 | 说明 |
|---|---|---|
options | ProjectServiceOptions | 粒度化配置项,详见下文第四节 |
jsDocParsingMode | ts.JSDocParsingMode | 控制解析 JSDoc 注释的激进程度(如'all' \| 'none' \| 'type-info'),对应 parser-options.ts 中的JSDocParsingMode |
tsconfigRootDir | string | tsconfig.json 的根目录,默认取当前目录 |
host | Partial<ts.server.ServerHost> | 自定义 Project Service 宿主,默认是ts.sys加桩(stub)文件监听器 |
3.2 返回值ProjectServiceAndMetadata
返回值除了service本身,还携带三项元数据(见 createProjectService.ts):
allowDefaultProject: string[] | undefined:允许从默认项目加载的文件 glob 列表(如果指定过);lastReloadTimestamp: number:上一次服务重载的performance.now()时间戳;maximumDefaultProjectFileMatchCount: number:默认项目最多可匹配的文件数,默认阈值见下文;service: TypeScriptProjectService:创建的 TypeScript Project Service 实例。
3.3 默认行为的源码级实现
在createProjectService内部,有几处值得注意的实现细节(createProjectService.ts):
(1)惰性加载 tsserverlibrary:TypeScript 的语言服务 API 位于typescript/lib/tsserverlibrary,代码采用require()惰性加载,避免未使用该服务的用户承担加载成本。
(2)不监听磁盘文件:源码注释明确说明"我们不监听磁盘,只在 ESLint 调用我们时引用这些文件"。因此system中的watchDirectory与watchFile被替换为createStubFileWatcher(一个close空操作的桩对象),这保证了 ESLint 命令行进程不会因为文件监听而无法退出。
(3)默认禁用 TypeScript 插件:除非传入loadTypeScriptPlugins,否则宿主系统的require被替换为一个恒返回错误信息"TypeScript plugins are not required when using parserOptions.projectService."的桩函数。这样做的目的是防止插件注册持久化的磁盘监听器(源码注释引用了 issue #9905)。
(4)ProjectService 实例化参数:cancellationToken恒返回false(不取消)、useInferredProjectPerProjectRoot: false、useSingleInferredProject: false,即不启用"每个目录独立推断项目"与"单一推断项目"。
(5)关闭 package.json 自动导入:调用service.setHostConfiguration({ preferences: { includePackageJsonAutoImports: 'off' } }),避免 Linting 场景下的多余类型解析。
(6)应用默认项目编译选项:若tsconfig.json解析成功,会将其options通过setCompilerOptionsForInferredProjects设置为推断项目的编译选项(源码注释指出这是对 TypeScript 内部 API 的硬断言式用法)。
上述默认行为均有对应的单元测试覆盖,可参考 tests/createProjectService.test.ts,例如"提供 stub require 当 loadTypeScriptPlugins 为假""不返回日志文件名""监听器来自自定义 host"等用例。
四、ProjectServiceOptions四个配置项详解
ProjectServiceOptions定义在 packages/types/src/parser-options.ts,并通过export { type ProjectServiceOptions } from '@typescript-eslint/types'再导出。四个选项的语义与 docs/packages/Parser.mdx 中的说明完全一致:
4.1allowDefaultProject(默认[])
Glob 数组,允许这些文件即使未被 Project Service 匹配,也使用默认项目的编译选项运行并获取类型信息。路径相对于tsconfigRootDir解析。典型用途:为eslint.config.js这类未包含在tsconfig.json中的配置文件提供类型信息。
该选项有两个强限制(由 validateDefaultProjectForFilesGlob.ts 强制校验):
- 禁止
**:任何包含**的 glob 都会直接抛错; - 禁止裸
*:glob === '*'同样抛错,错误信息会附上性能警告与指引。
原因是"每个从默认项目获取类型信息的文件都会给 Linting 带来不小的性能开销",官方要求克制使用。此外,useProgramFromProjectService中还有一条校验:某文件若既匹配allowDefaultProject、又被 Project Service 找到所属 tsconfig,会抛错提示"请把它从 allowDefaultProject 移除"(见 useProgramFromProjectService.ts)。
4.2defaultProject(默认'tsconfig.json')
指定一个替代 TypeScript 默认项目配置的 tsconfig 路径,相对tsconfigRootDir解析。注意文档特别强调:它只影响由allowDefaultProject纳入的"项目外文件",即这些文件的编译选项取自该 tsconfig。在 createProjectService.ts 中,该路径会被交给getParsedConfigFileFromTSServer解析,后者封装了@typescript-eslint/tsconfig-utils的getParsedConfigFile;若用户显式指定了defaultProject(而非默认值)而解析失败,会直接抛错:
Could not read Project Service default project '<路径>': <原始错误信息>4.3loadTypeScriptPlugins(默认false)
是否允许加载 tsconfig 中配置的 TypeScript 插件。默认关闭以防插件注册文件监听器导致 ESLint 命令行进程无法退出。文档给出一个非常实用的编辑器场景配置——只在 VS Code 内启用:
parserOptions: { projectService: { loadTypeScriptPlugins: !!process.env.VSCODE_PID, } }对应测试"不提供 require 到宿主系统当 loadTypeScriptPlugins 为假 / 提供真实 require 当为真"(见 createProjectService.test.ts)。
4.4maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING(默认8)
allowDefaultProject最多可匹配的文件数上限。这个选项名本身就是一条警告——"这会让 Linting 变慢"。源码中对应的默认阈值为常量DEFAULT_PROJECT_MATCHED_FILES_THRESHOLD = 8(createProjectService.ts),通过??空值合并逻辑取用户传入值或默认值。
当匹配默认项目的文件数超过该上限时,解析器会抛出错误,列出匹配到的文件(最多展示 20 个,超出部分用"...and N more files"概括),并提示"如确实需要,请调大该选项,或向 typescript-eslint 提交 issue 说明原因,以便社区帮你避免使用它"。该逻辑实现在 useProgramFromProjectService.ts。
五、与解析器集成:parserOptions.projectService的调用链
@typescript-eslint/project-service是解析器类型信息管线的"心脏"。@typescript-eslint/parser与@typescript-eslint/typescript-estree通过 parser-options.ts 中的projectService?: boolean | ProjectServiceOptions接收配置:true表示启用并全部走默认值,传对象则可自定义上述选项。
5.1 配置示例(来自官方文档)
Flat Config 风格:
// eslint.config.js export default [ { languageOptions: { parserOptions: { projectService: true, }, }, }, ];Legacy Config 风格:
// .eslintrc.js module.exports = { parser: '@typescript-eslint/parser', parserOptions: { projectService: true, }, };带自定义选项:
{ parser: '@typescript-eslint/parser', parserOptions: { projectService: { allowDefaultProject: ['*.js'], }, }, }5.2 底层调用链:useProgramFromProjectService
解析器侧的核心入口是 typescript-estree/src/useProgramFromProjectService.ts,其流程完整映射了官方 README 的示例:
- 更新扩展名配置:调用
service.setHostConfiguration同步extraFileExtensions,如检测到变化会触发项目重载; - 绝对路径化:将
filePath转为绝对路径(相对路径基于宿主当前目录拼接),并显式跳过文件名规范化以避免性能回归; - 判断默认项目放行:用
minimatch(带dot: true)将文件相对路径与allowDefaultProject的 glob 逐一匹配; - 分支处理:
- 不需要完整类型信息且未被放行 → 走
createNoProgramWithProjectService,如果服务已知该文件还会openClientFile刷新内容,但返回无类型信息的"无 Program"结果(保证非类型化规则依然可用、且不浪费构建开销); - 需要类型信息 → 调用
openClientFileFromProjectService打开文件。若找不到所属 tsconfig,会抛出"未找到"错误并给出三种排查方向:扩展名非标准时提示配置extraFileExtensions;否则提示"加入 tsconfig.json 或加入 allowDefaultProject";若allowDefaultProject已配置但未匹配,还会把 glob 与相对路径打印出来方便核对; - 重载兜底:编辑器场景下若打开失败且非 single-run 模式,且距上次重载超过 250ms(
RELOAD_THROTTLE_MS),会调用service.reloadProjects()刷新后重试;
- 不需要完整类型信息且未被放行 → 走
- 提取 Program:最终通过
getScriptInfo→getDefaultProjectForFile(fileName, true)→getLanguageService(true).getProgram()拿到类型化Program,交给createProjectProgram生成 AST 与程序的组合。
5.3 与project选项的关系
官方文档明确指出:projectService与project同时启用会报错(提示"Enabling 'project' does nothing when 'projectService' is enabled")。启用projectService时建议移除project。二者对比,projectService的两大优势是:
- 配置更简单:大多数项目无需显式配置
project路径或创建tsconfig.eslint.json; - 可预测性更强:它使用与编辑器完全相同的类型信息服务,与你在编辑器中看到的类型提示保持一致性。
5.4 常用集成场景
- 自定义规则测试:编写类型感知规则测试时,可对每个测试文件使用
parserOptions.projectService配合allowDefaultProject(见 docs/developers/Custom_Rules.mdx); - RuleTester:
@typescript-eslint/rule-tester同样支持在parserOptions.projectService下测试类型感知规则(见 docs/packages/Rule_Tester.mdx); - Monorepo:使用
parserOptions.projectService时无需再处理project在多包场景下的路径配置问题(见 docs/troubleshooting/typed-linting/Monorepos.mdx); - 关闭类型感知:可用
projectService: false(默认值)关闭,此时项目仍可用 ESLint 但不提供类型信息。
六、调试与常见问题
6.1 调试命名空间
该包使用debug库输出日志,命名空间前缀为typescript-eslint:project-service:*,共分五类(定义于 createProjectService.ts):
| 命名空间 | 内容 |
|---|---|
typescript-eslint:project-service:createProjectService | 服务创建过程与配置对象 |
typescript-eslint:project-service:tsserver:err | tsserver 错误级日志 |
typescript-eslint:project-service:tsserver:info | tsserver 信息级日志 |
typescript-eslint:project-service:tsserver:perf | tsserver 性能日志 |
typescript-eslint:project-service:tsserver:event | tsserver 事件(如projectLoadingStart) |
解析器侧还另有一个typescript-eslint:typescript-estree:useProgramFromProjectService命名空间。测试中验证了:只有启用对应命名空间时logger.loggingEnabled()才为true,且事件处理器也仅在tsserver:event启用时才被挂载(见 createProjectService.test.ts)。
启用示例(在运行 ESLint 前设置环境变量):
DEBUG="typescript-eslint:project-service:*" eslint .6.2 常见错误与排查
- "XXX was not found by the project service":文件既不在任何 tsconfig 项目中,也不匹配
allowDefaultProject。按错误信息提示,要么把文件加入 tsconfig.json,要么加入allowDefaultProject;若扩展名非标准(如.vue、.md),需配置parserOptions.extraFileExtensions; - "Too many files (>N) have matched the default project":匹配默认项目的文件超过阈值(默认 8)。先检查
allowDefaultProjectglob 是否过宽(*与**本身就会被拒绝),再考虑调大maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING,并清楚知晓这会让 Linting 变慢; - "also was found in the project service":文件既匹配
allowDefaultProject又属于某个 tsconfig 项目,属冗余配置,删除allowDefaultProject中的对应条目即可。
6.3 版本与安装约束
根据 packages/project-service/package.json:
- Peer 依赖:
typescript >=4.8.4 <6.1.0; - Node 版本:
^18.18.0 || ^20.9.0 || >=21.1.0; - 模块格式为 CommonJS,
exports提供./dist/index.js与./dist/index.d.ts; - 依赖仅三个:
@typescript-eslint/tsconfig-utils(tsconfig 解析)、@typescript-eslint/types(类型定义)、debug(日志)。
七、总结
@typescript-eslint/project-service是一个体积小巧(公共 API 只有一个createProjectService)但定位关键的独立包:它把 TypeScript 语言服务端(tsserver)的 Project Service 封装成适合 Linting 进程的形式——不监听磁盘、默认禁用插件、惰性加载 tsserverlibrary,并以ProjectServiceAndMetadata的形式返回服务实例与运行时元数据。在 typescript-eslint 的整体架构中,它是parserOptions.projectService的底层实现,负责把"编辑器同款类型信息"稳定地带给类型化 ESLint 规则,并通过对allowDefaultProject的严格约束与阈值限制,在类型覆盖范围与 Linting 性能之间建立了一道可控的边界。
如果想要继续深入,建议按以下路径阅读源码:
- 服务创建与默认行为:packages/project-service/src/createProjectService.ts
- 配置选项类型定义:packages/types/src/parser-options.ts
- 解析器侧集成调用链:packages/typescript-estree/src/useProgramFromProjectService.ts
- 行为测试用例:packages/project-service/tests/createProjectService.test.ts
- 官方用户文档:docs/packages/Parser.mdx 与 docs/packages/Project_Service.mdx
- 类型化 Linting 入门:docs/getting-started/Typed_Linting.mdx
【免费下载链接】typescript-eslint:sparkles: Monorepo for all the tooling which enables ESLint to support TypeScript项目地址: https://gitcode.com/GitHub_Trending/ty/typescript-eslint
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考